This commit is contained in:
CookieCat
2024-01-17 09:50:10 -05:00
101 changed files with 5044 additions and 514 deletions

View File

@@ -0,0 +1,18 @@
<component name="ProjectRunConfigurationManager">
<configuration default="false" name="Archipelago Unittests" type="tests" factoryName="Unittests">
<module name="Archipelago" />
<option name="INTERPRETER_OPTIONS" value="" />
<option name="PARENT_ENVS" value="true" />
<option name="SDK_HOME" value="" />
<option name="WORKING_DIRECTORY" value="$PROJECT_DIR$" />
<option name="IS_MODULE_SDK" value="true" />
<option name="ADD_CONTENT_ROOTS" value="true" />
<option name="ADD_SOURCE_ROOTS" value="true" />
<EXTENSION ID="PythonCoverageRunConfigurationExtension" runner="coverage.py" />
<option name="_new_pattern" value="&quot;&quot;" />
<option name="_new_additionalArguments" value="&quot;&quot;" />
<option name="_new_target" value="&quot;$PROJECT_DIR$/test&quot;" />
<option name="_new_targetType" value="&quot;PATH&quot;" />
<method v="2" />
</configuration>
</component>

View File

@@ -460,7 +460,7 @@ class CommonContext:
else:
self.update_game(cached_game)
if needed_updates:
await self.send_msgs([{"cmd": "GetDataPackage", "games": list(needed_updates)}])
await self.send_msgs([{"cmd": "GetDataPackage", "games": [game_name]} for game_name in needed_updates])
def update_game(self, game_package: dict):
for item_name, item_id in game_package["item_name_to_id"].items():
@@ -477,6 +477,7 @@ class CommonContext:
current_cache = Utils.persistent_load().get("datapackage", {}).get("games", {})
current_cache.update(data_package["games"])
Utils.persistent_store("datapackage", "games", current_cache)
logger.info(f"Got new ID/Name DataPackage for {', '.join(data_package['games'])}")
for game, game_data in data_package["games"].items():
Utils.store_data_package_for_checksum(game, game_data)
@@ -727,7 +728,6 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
await ctx.server_auth(args['password'])
elif cmd == 'DataPackage':
logger.info("Got new ID/Name DataPackage")
ctx.consume_network_data_package(args['data'])
elif cmd == 'ConnectionRefused':

12
Main.py
View File

@@ -114,7 +114,9 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
for _ in range(count):
world.push_precollected(world.create_item(item_name, player))
for item_name, count in world.start_inventory_from_pool.setdefault(player, StartInventoryPool({})).value.items():
for item_name, count in getattr(world.worlds[player].options,
"start_inventory_from_pool",
StartInventoryPool({})).value.items():
for _ in range(count):
world.push_precollected(world.create_item(item_name, player))
# remove from_pool items also from early items handling, as starting is plenty early.
@@ -167,10 +169,14 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
# remove starting inventory from pool items.
# Because some worlds don't actually create items during create_items this has to be as late as possible.
if any(world.start_inventory_from_pool[player].value for player in world.player_ids):
if any(getattr(world.worlds[player].options, "start_inventory_from_pool", None) for player in world.player_ids):
new_items: List[Item] = []
depletion_pool: Dict[int, Dict[str, int]] = {
player: world.start_inventory_from_pool[player].value.copy() for player in world.player_ids}
player: getattr(world.worlds[player].options,
"start_inventory_from_pool",
StartInventoryPool({})).value.copy()
for player in world.player_ids
}
for player, items in depletion_pool.items():
player_world: AutoWorld.World = world.worlds[player]
for count in items.values():

View File

@@ -4,14 +4,29 @@ import subprocess
import multiprocessing
import warnings
local_dir = os.path.dirname(__file__)
requirements_files = {os.path.join(local_dir, 'requirements.txt')}
if sys.version_info < (3, 8, 6):
raise RuntimeError("Incompatible Python Version. 3.8.7+ is supported.")
# don't run update if environment is frozen/compiled or if not the parent process (skip in subprocess)
update_ran = getattr(sys, "frozen", False) or multiprocessing.parent_process()
_skip_update = bool(getattr(sys, "frozen", False) or multiprocessing.parent_process())
update_ran = _skip_update
class RequirementsSet(set):
def add(self, e):
global update_ran
update_ran &= _skip_update
super().add(e)
def update(self, *s):
global update_ran
update_ran &= _skip_update
super().update(*s)
local_dir = os.path.dirname(__file__)
requirements_files = RequirementsSet((os.path.join(local_dir, 'requirements.txt'),))
if not update_ran:
for entry in os.scandir(os.path.join(local_dir, "worlds")):

View File

@@ -2210,25 +2210,24 @@ def parse_args() -> argparse.Namespace:
async def auto_shutdown(ctx, to_cancel=None):
await asyncio.sleep(ctx.auto_shutdown)
def inactivity_shutdown():
ctx.server.ws_server.close()
ctx.exit_event.set()
if to_cancel:
for task in to_cancel:
task.cancel()
logging.info("Shutting down due to inactivity.")
while not ctx.exit_event.is_set():
if not ctx.client_activity_timers.values():
ctx.server.ws_server.close()
ctx.exit_event.set()
if to_cancel:
for task in to_cancel:
task.cancel()
logging.info("Shutting down due to inactivity.")
inactivity_shutdown()
else:
newest_activity = max(ctx.client_activity_timers.values())
delta = datetime.datetime.now(datetime.timezone.utc) - newest_activity
seconds = ctx.auto_shutdown - delta.total_seconds()
if seconds < 0:
ctx.server.ws_server.close()
ctx.exit_event.set()
if to_cancel:
for task in to_cancel:
task.cancel()
logging.info("Shutting down due to inactivity.")
inactivity_shutdown()
else:
await asyncio.sleep(seconds)

View File

@@ -58,6 +58,7 @@ Currently, the following games are supported:
* Heretic
* Landstalker: The Treasures of King Nole
* Final Fantasy Mystic Quest
* TUNIC
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled

View File

@@ -20,8 +20,8 @@ def generate_api():
race = False
meta_options_source = {}
if 'file' in request.files:
file = request.files['file']
options = get_yaml_data(file)
files = request.files.getlist('file')
options = get_yaml_data(files)
if isinstance(options, Markup):
return {"text": options.striptags()}, 400
if isinstance(options, str):

View File

@@ -69,8 +69,8 @@
</td>
<td>
<select name="collect_mode" id="collect_mode">
<option value="goal">Allow !collect after goal completion</option>
<option value="auto">Automatic on goal completion</option>
<option value="goal">Allow !collect after goal completion</option>
<option value="auto-enabled">
Automatic on goal completion and manual !collect
</option>
@@ -93,9 +93,9 @@
{% if race -%}
<option value="disabled">Disabled in Race mode</option>
{%- else -%}
<option value="disabled">Disabled</option>
<option value="goal">Allow !remaining after goal completion</option>
<option value="enabled">Manual !remaining</option>
<option value="disabled">Disabled</option>
{%- endif -%}
</select>
</td>
@@ -185,12 +185,12 @@ Warning: playthrough can take a significant amount of time for larger multiworld
</span>
</td>
<td>
<input type="checkbox" id="plando_items" name="plando_items" value="items">
<label for="plando_items">Items</label><br>
<input type="checkbox" id="plando_bosses" name="plando_bosses" value="bosses" checked>
<label for="plando_bosses">Bosses</label><br>
<input type="checkbox" id="plando_items" name="plando_items" value="items" checked>
<label for="plando_items">Items</label><br>
<input type="checkbox" id="plando_connections" name="plando_connections" value="connections" checked>
<label for="plando_connections">Connections</label><br>

View File

@@ -1,6 +1,6 @@
{% block footer %}
<footer id="island-footer">
<div id="copyright-notice">Copyright 2023 Archipelago</div>
<div id="copyright-notice">Copyright 2024 Archipelago</div>
<div id="links">
<a href="/sitemap">Site Map</a>
-

View File

@@ -1,4 +1,5 @@
import datetime
import collections
from dataclasses import dataclass
from typing import Any, Callable, Dict, List, Optional, Set, Tuple
from uuid import UUID
@@ -8,7 +9,7 @@ from werkzeug.exceptions import abort
from MultiServer import Context, get_saving_second
from NetUtils import ClientStatus, Hint, NetworkItem, NetworkSlot, SlotType
from Utils import restricted_loads
from Utils import restricted_loads, KeyedDefaultDict
from . import app, cache
from .models import GameDataPackage, Room
@@ -62,12 +63,18 @@ class TrackerData:
self.location_name_to_id: Dict[str, Dict[str, int]] = {}
# Generate inverse lookup tables from data package, useful for trackers.
self.item_id_to_name: Dict[str, Dict[int, str]] = {}
self.location_id_to_name: Dict[str, Dict[int, str]] = {}
self.item_id_to_name: Dict[str, Dict[int, str]] = KeyedDefaultDict(lambda game_name: {
game_name: KeyedDefaultDict(lambda code: f"Unknown Game {game_name} - Item (ID: {code})")
})
self.location_id_to_name: Dict[str, Dict[int, str]] = KeyedDefaultDict(lambda game_name: {
game_name: KeyedDefaultDict(lambda code: f"Unknown Game {game_name} - Location (ID: {code})")
})
for game, game_package in self._multidata["datapackage"].items():
game_package = restricted_loads(GameDataPackage.get(checksum=game_package["checksum"]).data)
self.item_id_to_name[game] = {id: name for name, id in game_package["item_name_to_id"].items()}
self.location_id_to_name[game] = {id: name for name, id in game_package["location_name_to_id"].items()}
self.item_id_to_name[game] = KeyedDefaultDict(lambda code: f"Unknown Item (ID: {code})", {
id: name for name, id in game_package["item_name_to_id"].items()})
self.location_id_to_name[game] = KeyedDefaultDict(lambda code: f"Unknown Location (ID: {code})", {
id: name for name, id in game_package["location_name_to_id"].items()})
# Normal lookup tables as well.
self.item_name_to_id[game] = game_package["item_name_to_id"]
@@ -115,10 +122,10 @@ class TrackerData:
return self._multisave.get("received_items", {}).get((team, player, True), [])
@_cache_results
def get_player_inventory_counts(self, team: int, player: int) -> Dict[int, int]:
def get_player_inventory_counts(self, team: int, player: int) -> collections.Counter:
"""Retrieves a dictionary of all items received by their id and their received count."""
items = self.get_player_received_items(team, player)
inventory = {item: 0 for item in self.item_id_to_name[self.get_player_game(team, player)]}
inventory = collections.Counter()
for item in items:
inventory[item.item] += 1
@@ -149,16 +156,15 @@ class TrackerData:
"""Retrieves a dictionary of number of completed worlds per team."""
return {
team: sum(
self.get_player_client_status(team, player) == ClientStatus.CLIENT_GOAL
for player in players if self.get_slot_info(team, player).type == SlotType.player
) for team, players in self.get_team_players().items()
self.get_player_client_status(team, player) == ClientStatus.CLIENT_GOAL for player in players
) for team, players in self.get_all_players().items()
}
@_cache_results
def get_team_hints(self) -> Dict[int, Set[Hint]]:
"""Retrieves a dictionary of all hints per team."""
hints = {}
for team, players in self.get_team_players().items():
for team, players in self.get_all_slots().items():
hints[team] = set()
for player in players:
hints[team] |= self.get_player_hints(team, player)
@@ -170,7 +176,7 @@ class TrackerData:
"""Retrieves a dictionary of total player locations each team has."""
return {
team: sum(len(self.get_player_locations(team, player)) for player in players)
for team, players in self.get_team_players().items()
for team, players in self.get_all_players().items()
}
@_cache_results
@@ -178,16 +184,30 @@ class TrackerData:
"""Retrieves a dictionary of checked player locations each team has."""
return {
team: sum(len(self.get_player_checked_locations(team, player)) for player in players)
for team, players in self.get_team_players().items()
for team, players in self.get_all_players().items()
}
# TODO: Change this method to properly build for each team once teams are properly implemented, as they don't
# currently exist in multidata to easily look up, so these are all assuming only 1 team: Team #0
@_cache_results
def get_team_players(self) -> Dict[int, List[int]]:
def get_all_slots(self) -> Dict[int, List[int]]:
"""Retrieves a dictionary of all players ids on each team."""
return {
0: [player for player, slot_info in self._multidata["slot_info"].items()]
0: [
player for player, slot_info in self._multidata["slot_info"].items()
]
}
# TODO: Change this method to properly build for each team once teams are properly implemented, as they don't
# currently exist in multidata to easily look up, so these are all assuming only 1 team: Team #0
@_cache_results
def get_all_players(self) -> Dict[int, List[int]]:
"""Retrieves a dictionary of all player slot-type players ids on each team."""
return {
0: [
player for player, slot_info in self._multidata["slot_info"].items()
if self.get_slot_info(0, player).type == SlotType.player
]
}
@_cache_results
@@ -203,7 +223,7 @@ class TrackerData:
"""Retrieves a dictionary of all locations and their associated item metadata per player."""
return {
(team, player): self.get_player_locations(team, player)
for team, players in self.get_team_players().items() for player in players
for team, players in self.get_all_players().items() for player in players
}
@_cache_results
@@ -211,7 +231,7 @@ class TrackerData:
"""Retrieves a dictionary of games for each player."""
return {
(team, player): self.get_player_game(team, player)
for team, players in self.get_team_players().items() for player in players
for team, players in self.get_all_slots().items() for player in players
}
@_cache_results
@@ -219,7 +239,7 @@ class TrackerData:
"""Retrieves a dictionary of all locations complete per player."""
return {
(team, player): len(self.get_player_checked_locations(team, player))
for team, players in self.get_team_players().items() for player in players
for team, players in self.get_all_players().items() for player in players
}
@_cache_results
@@ -227,14 +247,14 @@ class TrackerData:
"""Retrieves a dictionary of all ClientStatus values per player."""
return {
(team, player): self.get_player_client_status(team, player)
for team, players in self.get_team_players().items() for player in players
for team, players in self.get_all_players().items() for player in players
}
@_cache_results
def get_room_long_player_names(self) -> Dict[TeamPlayer, str]:
"""Retrieves a dictionary of names with aliases for each player."""
long_player_names = {}
for team, players in self.get_team_players().items():
for team, players in self.get_all_slots().items():
for player in players:
alias = self.get_player_alias(team, player)
if alias:
@@ -370,7 +390,8 @@ def render_generic_multiworld_tracker(tracker_data: TrackerData, enabled_tracker
enabled_trackers=enabled_trackers,
current_tracker="Generic",
room=tracker_data.room,
room_players=tracker_data.get_team_players(),
all_slots=tracker_data.get_all_slots(),
room_players=tracker_data.get_all_players(),
locations=tracker_data.get_room_locations(),
locations_complete=tracker_data.get_room_locations_complete(),
total_team_locations=tracker_data.get_team_locations_total_count(),
@@ -389,7 +410,6 @@ def render_generic_multiworld_tracker(tracker_data: TrackerData, enabled_tracker
# TODO: This is a temporary solution until a proper Tracker API can be implemented for tracker templates and data to
# live in their respective world folders.
import collections
from worlds import network_data_package
@@ -400,7 +420,7 @@ if "Factorio" in network_data_package["games"]:
(team, player): {
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_team_players().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"
}
@@ -409,7 +429,8 @@ if "Factorio" in network_data_package["games"]:
enabled_trackers=enabled_trackers,
current_tracker="Factorio",
room=tracker_data.room,
room_players=tracker_data.get_team_players(),
all_slots=tracker_data.get_all_slots(),
room_players=tracker_data.get_all_players(),
locations=tracker_data.get_room_locations(),
locations_complete=tracker_data.get_room_locations_complete(),
total_team_locations=tracker_data.get_team_locations_total_count(),
@@ -547,7 +568,7 @@ if "A Link to the Past" in network_data_package["games"]:
if area_name != "Total" else tracker_data._multidata["checks_in_area"][player]["Total"]
for area_name in ordered_areas
}
for team, players in tracker_data.get_team_players().items()
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"
@@ -585,7 +606,7 @@ if "A Link to the Past" in network_data_package["games"]:
player_location_to_area = {
(team, player): _get_location_table(tracker_data._multidata["checks_in_area"][player])
for team, players in tracker_data.get_team_players().items()
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"
@@ -593,15 +614,15 @@ if "A Link to the Past" in network_data_package["games"]:
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_team_players().items()
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_team_players()[0]}
player_small_key_locations = {player: set() for player in tracker_data.get_team_players()[0]}
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()
@@ -639,7 +660,8 @@ if "A Link to the Past" in network_data_package["games"]:
enabled_trackers=enabled_trackers,
current_tracker="A Link to the Past",
room=tracker_data.room,
room_players=tracker_data.get_team_players(),
all_slots=tracker_data.get_all_slots(),
room_players=tracker_data.get_all_players(),
locations=tracker_data.get_room_locations(),
locations_complete=tracker_data.get_room_locations_complete(),
total_team_locations=tracker_data.get_team_locations_total_count(),

View File

@@ -164,6 +164,9 @@
# The Legend of Zelda (1)
/worlds/tloz/ @Rosalie-A @t3hf1gm3nt
# TUNIC
/worlds/tunic/ @silent-destroyer
# Undertale
/worlds/undertale/ @jonloveslegos

View File

@@ -675,8 +675,8 @@ Tags are represented as a list of strings, the common Client tags follow:
### DeathLink
A special kind of Bounce packet that can be supported by any AP game. It targets the tag "DeathLink" and carries the following data:
| Name | Type | Notes |
| ---- | ---- | ---- |
| time | float | Unix Time Stamp of time of death. |
| cause | str | Optional. Text to explain the cause of death, ex. "Berserker was run over by a train." |
| source | str | Name of the player who first died. Can be a slot name, but can also be a name from within a multiplayer game. |
| Name | Type | Notes |
|--------|-------|--------------------------------------------------------------------------------------------------------------------------------------------------------|
| time | float | Unix Time Stamp of time of death. |
| cause | str | Optional. Text to explain the cause of death. When provided, or checked, this should contain the player name, ex. "Berserker was run over by a train." |
| source | str | Name of the player who first died. Can be a slot name, but can also be a name from within a multiplayer game. |

View File

@@ -4,7 +4,7 @@ PyYAML>=6.0.1
jellyfish>=1.0.3
jinja2>=3.1.2
schema>=0.7.5
kivy>=2.2.1
kivy>=2.3.0
bsdiff4>=1.2.4
platformdirs>=4.0.0
certifi>=2023.11.17

View File

@@ -597,8 +597,8 @@ class ServerOptions(Group):
disable_item_cheat: Union[DisableItemCheat, bool] = False
location_check_points: LocationCheckPoints = LocationCheckPoints(1)
hint_cost: HintCost = HintCost(10)
release_mode: ReleaseMode = ReleaseMode("goal")
collect_mode: CollectMode = CollectMode("goal")
release_mode: ReleaseMode = ReleaseMode("auto")
collect_mode: CollectMode = CollectMode("auto")
remaining_mode: RemainingMode = RemainingMode("goal")
auto_shutdown: AutoShutdown = AutoShutdown(0)
compatibility: Compatibility = Compatibility(2)
@@ -673,7 +673,7 @@ class GeneratorOptions(Group):
spoiler: Spoiler = Spoiler(3)
glitch_triforce_room: GlitchTriforceRoom = GlitchTriforceRoom(1) # why is this here?
race: Race = Race(0)
plando_options: PlandoOptions = PlandoOptions("bosses")
plando_options: PlandoOptions = PlandoOptions("bosses, connections, texts")
class SNIOptions(Group):

View File

@@ -54,7 +54,6 @@ if __name__ == "__main__":
# TODO: move stuff to not require this
import ModuleUpdate
ModuleUpdate.update(yes="--yes" in sys.argv or "-y" in sys.argv)
ModuleUpdate.update_ran = False # restore for later
from worlds.LauncherComponents import components, icon_paths
from Utils import version_tuple, is_windows, is_linux
@@ -76,7 +75,6 @@ non_apworlds: set = {
"Ocarina of Time",
"Overcooked! 2",
"Raft",
"Secret of Evermore",
"Slay the Spire",
"Sudoku",
"Super Mario 64",
@@ -305,7 +303,6 @@ class BuildExeCommand(cx_Freeze.command.build_exe.BuildEXE):
print(f"Outputting to: {self.buildfolder}")
os.makedirs(self.buildfolder, exist_ok=True)
import ModuleUpdate
ModuleUpdate.requirements_files.add(os.path.join("WebHostLib", "requirements.txt"))
ModuleUpdate.update(yes=self.yes)
# auto-build cython modules

View File

@@ -285,7 +285,7 @@ class WorldTestBase(unittest.TestCase):
if not (self.run_default_tests and self.constructed):
return
with self.subTest("Game", game=self.game):
excluded = self.multiworld.exclude_locations[1].value
excluded = self.multiworld.worlds[1].options.exclude_locations.value
state = self.multiworld.get_all_state(False)
for location in self.multiworld.get_locations():
if location.name not in excluded:

127
test/benchmark/__init__.py Normal file
View File

@@ -0,0 +1,127 @@
import time
class TimeIt:
def __init__(self, name: str, time_logger=None):
self.name = name
self.logger = time_logger
self.timer = None
self.end_timer = None
def __enter__(self):
self.timer = time.perf_counter()
return self
@property
def dif(self):
return self.end_timer - self.timer
def __exit__(self, exc_type, exc_val, exc_tb):
if not self.end_timer:
self.end_timer = time.perf_counter()
if self.logger:
self.logger.info(f"{self.dif:.4f} seconds in {self.name}.")
if __name__ == "__main__":
import argparse
import logging
import gc
import collections
import typing
# makes this module runnable from its folder.
import sys
import os
sys.path.remove(os.path.dirname(__file__))
new_home = os.path.normpath(os.path.join(os.path.dirname(__file__), os.pardir, os.pardir))
os.chdir(new_home)
sys.path.append(new_home)
from Utils import init_logging, local_path
local_path.cached_path = new_home
from BaseClasses import MultiWorld, CollectionState, Location
from worlds import AutoWorld
from worlds.AutoWorld import call_all
init_logging("Benchmark Runner")
logger = logging.getLogger("Benchmark")
class BenchmarkRunner:
gen_steps: typing.Tuple[str, ...] = (
"generate_early", "create_regions", "create_items", "set_rules", "generate_basic", "pre_fill")
rule_iterations: int = 100_000
if sys.version_info >= (3, 9):
@staticmethod
def format_times_from_counter(counter: collections.Counter[str], top: int = 5) -> str:
return "\n".join(f" {time:.4f} in {name}" for name, time in counter.most_common(top))
else:
@staticmethod
def format_times_from_counter(counter: collections.Counter, top: int = 5) -> str:
return "\n".join(f" {time:.4f} in {name}" for name, time in counter.most_common(top))
def location_test(self, test_location: Location, state: CollectionState, state_name: str) -> float:
with TimeIt(f"{test_location.game} {self.rule_iterations} "
f"runs of {test_location}.access_rule({state_name})", logger) as t:
for _ in range(self.rule_iterations):
test_location.access_rule(state)
# if time is taken to disentangle complex ref chains,
# this time should be attributed to the rule.
gc.collect()
return t.dif
def main(self):
for game in sorted(AutoWorld.AutoWorldRegister.world_types):
summary_data: typing.Dict[str, collections.Counter[str]] = {
"empty_state": collections.Counter(),
"all_state": collections.Counter(),
}
try:
multiworld = MultiWorld(1)
multiworld.game[1] = game
multiworld.player_name = {1: "Tester"}
multiworld.set_seed(0)
multiworld.state = CollectionState(multiworld)
args = argparse.Namespace()
for name, option in AutoWorld.AutoWorldRegister.world_types[game].options_dataclass.type_hints.items():
setattr(args, name, {
1: option.from_any(getattr(option, "default"))
})
multiworld.set_options(args)
gc.collect()
for step in self.gen_steps:
with TimeIt(f"{game} step {step}", logger):
call_all(multiworld, step)
gc.collect()
locations = sorted(multiworld.get_unfilled_locations())
if not locations:
continue
all_state = multiworld.get_all_state(False)
for location in locations:
time_taken = self.location_test(location, multiworld.state, "empty_state")
summary_data["empty_state"][location.name] = time_taken
time_taken = self.location_test(location, all_state, "all_state")
summary_data["all_state"][location.name] = time_taken
total_empty_state = sum(summary_data["empty_state"].values())
total_all_state = sum(summary_data["all_state"].values())
logger.info(f"{game} took {total_empty_state/len(locations):.4f} "
f"seconds per location in empty_state and {total_all_state/len(locations):.4f} "
f"in all_state. (all times summed for {self.rule_iterations} runs.)")
logger.info(f"Top times in empty_state:\n"
f"{self.format_times_from_counter(summary_data['empty_state'])}")
logger.info(f"Top times in all_state:\n"
f"{self.format_times_from_counter(summary_data['all_state'])}")
except Exception as e:
logger.exception(e)
runner = BenchmarkRunner()
runner.main()

View File

@@ -1,5 +1,6 @@
import unittest
from worlds.AutoWorld import AutoWorldRegister
from worlds.AutoWorld import AutoWorldRegister, call_all
from . import setup_solo_multiworld
@@ -53,7 +54,7 @@ class TestBase(unittest.TestCase):
f"{game_name} Item count MUST meet or exceed the number of locations",
)
def testItemsInDatapackage(self):
def test_items_in_datapackage(self):
"""Test that any created items in the itempool are in the datapackage"""
for game_name, world_type in AutoWorldRegister.world_types.items():
with self.subTest("Game", game=game_name):
@@ -69,3 +70,20 @@ class TestBase(unittest.TestCase):
with self.subTest("Name should be valid", game=game_name, item=name):
self.assertIn(name, valid_names,
"All item descriptions must match defined item names")
def test_itempool_not_modified(self):
"""Test that worlds don't modify the itempool after `create_items`"""
gen_steps = ("generate_early", "create_regions", "create_items")
additional_steps = ("set_rules", "generate_basic", "pre_fill")
excluded_games = ("Links Awakening DX", "Ocarina of Time", "SMZ3")
worlds_to_test = {game: world
for game, world in AutoWorldRegister.world_types.items() if game not in excluded_games}
for game_name, world_type in worlds_to_test.items():
with self.subTest("Game", game=game_name):
multiworld = setup_solo_multiworld(world_type, gen_steps)
created_items = multiworld.itempool.copy()
for step in additional_steps:
with self.subTest("step", step=step):
call_all(multiworld, step)
self.assertEqual(created_items, multiworld.itempool,
f"{game_name} modified the itempool during {step}")

View File

@@ -10,3 +10,10 @@ class TestOptions(unittest.TestCase):
for option_key, option in world_type.options_dataclass.type_hints.items():
with self.subTest(game=gamename, option=option_key):
self.assertTrue(option.__doc__)
def test_options_are_not_set_by_world(self):
"""Test that options attribute is not already set"""
for gamename, world_type in AutoWorldRegister.world_types.items():
with self.subTest(game=gamename):
self.assertFalse(hasattr(world_type, "options"),
f"Unexpected assignment to {world_type.__name__}.options!")

View File

@@ -37,7 +37,7 @@ class TestBase(unittest.TestCase):
unreachable_regions = self.default_settings_unreachable_regions.get(game_name, set())
with self.subTest("Game", game=game_name):
world = setup_solo_multiworld(world_type)
excluded = world.exclude_locations[1].value
excluded = world.worlds[1].options.exclude_locations.value
state = world.get_all_state(False)
for location in world.get_locations():
if location.name not in excluded:

View File

@@ -1,5 +1,7 @@
import io
import unittest
import json
import yaml
class TestDocs(unittest.TestCase):
@@ -23,7 +25,7 @@ class TestDocs(unittest.TestCase):
response = self.client.post("/api/generate")
self.assertIn("No options found. Expected file attachment or json weights.", response.text)
def test_generation_queued(self):
def test_generation_queued_weights(self):
options = {
"Tester1":
{
@@ -40,3 +42,19 @@ class TestDocs(unittest.TestCase):
json_data = response.get_json()
self.assertTrue(json_data["text"].startswith("Generation of seed "))
self.assertTrue(json_data["text"].endswith(" started successfully."))
def test_generation_queued_file(self):
options = {
"game": "Archipelago",
"name": "Tester",
"Archipelago": {}
}
response = self.client.post(
"/api/generate",
data={
'file': (io.BytesIO(yaml.dump(options, encoding="utf-8")), "test.yaml")
},
)
json_data = response.get_json()
self.assertTrue(json_data["text"].startswith("Generation of seed "))
self.assertTrue(json_data["text"].endswith(" started successfully."))

View File

@@ -1,24 +1,12 @@
""" FillType_* is not a real kivy type - just something to fill unknown typing. """
from typing import Sequence
FillType_Vec = Sequence[int]
class FillType_Drawable:
def __init__(self, *, pos: FillType_Vec = ..., size: FillType_Vec = ...) -> None: ...
class FillType_Texture(FillType_Drawable):
pass
from .texture import FillType_Drawable, FillType_Vec, Texture
class FillType_Shape(FillType_Drawable):
texture: FillType_Texture
texture: Texture
def __init__(self,
*,
texture: FillType_Texture = ...,
texture: Texture = ...,
pos: FillType_Vec = ...,
size: FillType_Vec = ...) -> None: ...
@@ -35,6 +23,6 @@ class Rectangle(FillType_Shape):
def __init__(self,
*,
source: str = ...,
texture: FillType_Texture = ...,
texture: Texture = ...,
pos: FillType_Vec = ...,
size: FillType_Vec = ...) -> None: ...

View File

@@ -0,0 +1,13 @@
""" FillType_* is not a real kivy type - just something to fill unknown typing. """
from typing import Sequence
FillType_Vec = Sequence[int]
class FillType_Drawable:
def __init__(self, *, pos: FillType_Vec = ..., size: FillType_Vec = ...) -> None: ...
class Texture:
pass

View File

@@ -0,0 +1,9 @@
import io
from kivy.graphics.texture import Texture
class CoreImage:
texture: Texture
def __init__(self, data: io.BytesIO, ext: str) -> None: ...

View File

@@ -79,8 +79,8 @@ class AutoWorldRegister(type):
if "options_dataclass" not in dct and "option_definitions" in dct:
# TODO - switch to deprecate after a version
if __debug__:
from warnings import warn
warn("Assigning options through option_definitions is now deprecated. Use options_dataclass instead.")
logging.warning(f"{name} Assigned options through option_definitions which is now deprecated. "
"Please use options_dataclass instead.")
dct["options_dataclass"] = make_dataclass(f"{name}Options", dct["option_definitions"].items(),
bases=(PerGameCommonOptions,))
@@ -328,7 +328,7 @@ class World(metaclass=AutoWorldRegister):
def create_items(self) -> None:
"""
Method for creating and submitting items to the itempool. Items and Regions should *not* be created and submitted
Method for creating and submitting items to the itempool. Items and Regions must *not* be created and submitted
to the MultiWorld after this step. If items need to be placed during pre_fill use `get_prefill_items`.
"""
pass

View File

@@ -10,8 +10,7 @@ As we are using BizHawk, this guide is only applicable to Windows and Linux syst
- Version 2.3.1 and later are supported. Version 2.7 is recommended for stability.
- Detailed installation instructions for BizHawk can be found at the above link.
- Windows users must run the prereq installer first, which can also be found at the above link.
- The built-in Archipelago client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases)
(select `Adventure Client` during installation).
- The built-in Archipelago client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases).
- An Adventure NTSC ROM file. The Archipelago community cannot provide these.
## Configuring BizHawk

View File

@@ -1,21 +1,37 @@
# A Hat in Time
## Where is the settings page?
## Where is the options page?
The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
The [player options page for this game](../player-settings) contains all the options you need to configure and export a
config file.
## What does randomization do to this game?
Items which the player would normally acquire throughout the game have been moved around. Chapter costs are randomized in a progressive order based on your settings, so for example you could go to Subcon Forest -> Battle of the Birds -> Alpine Skyline, etc. in that order. If act shuffle is turned on, the levels and Time Rifts in these chapters will be randomized as well.
Items which the player would normally acquire throughout the game have been moved around.
Chapter costs are randomized in a progressive order based on your options,
so for example you could go to Subcon Forest -> Battle of the Birds -> Alpine Skyline, etc. in that order. If act shuffle is turned on,
the levels and Time Rifts in these chapters will be randomized as well.
To unlock and access a chapter's Time Rift in act shuffle, the levels in place of the original acts required to unlock the Time Rift in the vanilla game must be completed, and then you must enter a level that allows you to access that Time Rift. For example, Time Rift: Bazaar requires Heating Up Mafia Town to be completed in the vanilla game. To unlock this Time Rift in act shuffle (and therefore the level it contains) you must complete the level that was shuffled in place of Heating Up Mafia Town and then enter the Time Rift through a Mafia Town level.
To unlock and access a chapter's Time Rift in act shuffle,
the levels in place of the original acts required to unlock the Time Rift in the vanilla game must be completed,
and then you must enter a level that allows you to access that Time Rift.
For example, Time Rift: Bazaar requires Heating Up Mafia Town to be completed in the vanilla game.
To unlock this Time Rift in act shuffle (and therefore the level it contains)
you must complete the level that was shuffled in place of Heating Up Mafia Town
and then enter the Time Rift through a Mafia Town level.
## What items and locations get shuffled?
Time Pieces, Relics, Yarn, Badges, and most other items are shuffled. Unlike in the vanilla game, yarn is typeless, and hats will be automatically stitched in a set order once you gather enough yarn for each hat. Hats can also optionally be shuffled as individual items instead. Any items in the world, shops, act completions, and optionally storybook pages or Death Wish contracts are locations.
Time Pieces, Relics, Yarn, Badges, and most other items are shuffled.
Unlike in the vanilla game, yarn is typeless, and hats will be automatically stitched
in a set order once you gather enough yarn for each hat.
Hats can also optionally be shuffled as individual items instead.
Any items in the world, shops, act completions,
and optionally storybook pages or Death Wish contracts are locations.
Any freestanding items that are considered to be progression or useful will have a rainbow streak particle attached to them. Filler items will have a white glow attached to them instead.
Any freestanding items that are considered to be progression or useful
will have a rainbow streak particle attached to them.
Filler items will have a white glow attached to them instead.
## Which items can be in another player's world?
@@ -32,4 +48,4 @@ When the player receives an item, it will play the item collect effect and infor
## Is the DLC required to play A Hat in Time in Archipelago?
No, the DLC expansions are not required to play. Their content can be enabled through certain options that are disabled by default, but please don't turn them on if you don't own the respective DLC.
No, the DLC expansions are not required to play. Their content can be enabled through certain options that are disabled by default, but please don't turn them on if you don't own the respective DLC.

View File

@@ -33,7 +33,7 @@ To connect to the multiworld server, simply run the **ArchipelagoAHITClient** an
Commands will not work on the title screen, you must be in-game to use them. To use console commands, make sure ***Enable Developer Console*** is checked in Game Settings and press the tilde key or TAB while in-game.
`ap_say <message>` - Send a chat message to the server. Supports commands, such as !hint or !release.
`ap_say <message>` - Send a chat message to the server. Supports commands, such as `!hint` or `!release`.
`ap_deathlink` - Toggle Death Link.

View File

@@ -2,8 +2,7 @@
## Required Software
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). Make sure to check the box for
`SNI Client - A Link to the Past Patch Setup`
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases).
- [SNI](https://github.com/alttpo/sni/releases). This is automatically included with your Archipelago installation above.
- SNI is not compatible with (Q)Usb2Snes.
- Hardware or software capable of loading and playing SNES ROM files
@@ -18,11 +17,12 @@ but it is not supported.**
## Installation Procedures
1. Download and install SNIClient from the link above, making sure to install the most recent version.
**The installer file is located in the assets section at the bottom of the version information**.
- During setup, you will be asked to locate your base ROM file. This is your Japanese Link to the Past ROM file.
1. Download and install [Archipelago](<https://github.com/ArchipelagoMW/Archipelago/releases/latest>). **The installer
file is located in the assets section at the bottom of the version information.**
2. The first time you do local generation or patch your game, you will be asked to locate your base ROM file.
This is your Japanese Link to the Past ROM file. This only needs to be done once.
2. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM
3. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM
files.
1. Extract your emulator's folder to your Desktop, or somewhere you will remember.
2. Right-click on a ROM file and select **Open with...**

View File

@@ -5,7 +5,6 @@
- ChecksFinder from
the [Github releases Page for the game](https://github.com/jonloveslegos/ChecksFinder/releases) (latest version)
- Archipelago from the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases)
- (select `ChecksFinder Client` during installation.)
## Configuring your YAML file

View File

@@ -2,7 +2,7 @@
## Required Software
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). Make sure to check the box for `SNI Client - Donkey Kong Country 3 Patch Setup`
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases).
- Hardware or software capable of loading and playing SNES ROM files
@@ -23,9 +23,10 @@
### Windows Setup
1. During the installation of Archipelago, you will have been asked to install the SNI Client. If you did not do this,
or you are on an older version, you may run the installer again to install the SNI Client.
2. During setup, you will be asked to locate your base ROM file. This is your Donkey Kong Country 3 ROM file.
1. Download and install [Archipelago](<https://github.com/ArchipelagoMW/Archipelago/releases/latest>). **The installer
file is located in the assets section at the bottom of the version information.**
2. The first time you do local generation or patch your game, you will be asked to locate your base ROM file.
This is your Donkey Kong Country 3 ROM file. This only needs to be done once.
3. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM
files.
1. Extract your emulator's folder to your Desktop, or somewhere you will remember.

View File

@@ -13,14 +13,23 @@ client_version = 0
class DLCqwebworld(WebWorld):
tutorials = [Tutorial(
setup_en = Tutorial(
"Multiworld Setup Tutorial",
"A guide to setting up the Archipelago DLCQuest game on your computer.",
"English",
"setup_en.md",
"setup/en",
["axe_y"]
)]
)
setup_fr = Tutorial(
"Guide de configuration MultiWorld",
"Un guide pour configurer DLCQuest sur votre PC.",
"Français",
"setup_fr.md",
"setup/fr",
["Deoxis"]
)
tutorials = [setup_en, setup_fr]
class DLCqworld(World):

View File

@@ -0,0 +1,49 @@
# DLC Quest
## Où se trouve la page des paramètres ?
La [page des paramètres du joueur pour ce jeu](../player-settings) contient tous les paramètres dont vous avez besoin pour configurer et exporter le fichier.
## Quel est l'effet de la randomisation sur ce jeu ?
Les DLC seront obtenus en tant que check pour le multiworld. Il existe également d'autres checks optionnels dans DLC Quest.
## Quel est le but de DLC Quest ?
DLC Quest a deux campagnes, et le joueur peut choisir celle qu'il veut jouer pour sa partie.
Il peut également choisir de faire les deux campagnes.
## Quels sont les emplacements dans DLC quest ?
Les emplacements dans DLC Quest comprennent toujours
- les achats de DLC auprès du commerçant
- Les objectifs liés aux récompenses
- Tuer des moutons dans DLC Quest
- Objectifs spécifiques de l'attribution dans Live Freemium or Die
Il existe également un certain nombres de critères de localisation qui sont optionnels et que les joueurs peuvent choisir d'inclure ou non dans leur sélection :
- Objets que votre personnage peut obtenir de différentes manières
- Swords
- Gun
- Box of Various Supplies
- Humble Indie Bindle
- Pickaxe
- Coinsanity : Pièces de monnaie, soit individuellement, soit sous forme de lots personnalisés
## Quels objets peuvent se trouver dans le monde d'un autre joueur ?
Tous les DLC du jeu sont mélangés dans le stock d'objets. Les objets liés aux contrôles optionnels décrits ci-dessus sont également dans le stock
Il y a aussi de nouveaux objets pièges, utilisés comme substituts, basés sur les désagréments du jeu vanille.
- Zombie Sheep
- Loading Screens
- Temporary Spikes
## Que se passe-t-il lorsque le joueur reçoit un objet ?
Chaque fois qu'un objet est reçu en ligne, une notification apparaît à l'écran pour en informer le joueur.
Certains objets sont accompagnés d'une animation ou d'une scène qui se déroule immédiatement après leur réception.
Les objets reçus hors ligne ne sont pas accompagnés d'une animation ou d'une scène, et sont simplement activés lors de la connexion.

View File

@@ -0,0 +1,55 @@
# # Guide de configuration MultiWorld de DLCQuest
## Logiciels requis
- DLC Quest sur PC (Recommandé: [Version Steam](https://store.steampowered.com/app/230050/DLC_Quest/))
- [DLCQuestipelago](https://github.com/agilbert1412/DLCQuestipelago/releases)
- BepinEx (utilisé comme un modloader pour DLCQuest. La version du mod ci-dessus inclut BepInEx si vous choisissez la version d'installation complète)
## Logiciels optionnels
- [Archipelago] (https://github.com/ArchipelagoMW/Archipelago/releases)
- (Uniquement pour le TextClient)
## Créer un fichier de configuration (.yaml)
### Qu'est-ce qu'un fichier YAML et pourquoi en ai-je besoin ?
Voir le guide d'Archipelago sur la mise en place d'un YAML de base : [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en)
### Où puis-je obtenir un fichier YAML ?
Vous pouvez personnaliser vos paramètres en visitant la [page des paramètres du joueur DLC Quest] (/games/DLCQuest/player-settings).
## Rejoindre une partie multi-monde
### Installer le mod
- Télécharger le [DLCQuestipelago mod release](https://github.com/agilbert1412/DLCQuestipelago/releases). Si c'est la première fois que vous installez le mod, ou si vous n'êtes pas à l'aise avec l'édition manuelle de fichiers, vous devriez choisir l'Installateur. Il se chargera de la plus grande partie du travail pour vous
- Extraire l'archive .zip à l'emplacement de votre choix
- Exécutez "DLCQuestipelagoInstaller.exe".
![image](https://i.imgur.com/2sPhMgs.png)
- Le programme d'installation devrait décrire ce qu'il fait à chaque étape, et vous demandera votre avis si nécessaire.
- Il vous permettra de choisir l'emplacement d'installation de votre jeu moddé et vous proposera un emplacement par défaut
- Il **essayera** de trouver votre jeu DLCQuest sur votre ordinateur et, en cas d'échec, vous demandera d'indiquer le chemin d'accès.
- Il vous offrira la possibilité de créer un raccourci sur le bureau pour le lanceur moddé.
### Se connecter au MultiServer
- Localisez le fichier "ArchipelagoConnectionInfo.json", qui se situe dans le même emplacement que votre installation moddée. Vous pouvez éditer ce fichier avec n'importe quel éditeur de texte, et vous devez entrer l'adresse IP du serveur, le port et votre nom de joueur dans les champs appropriés.
- Exécutez BepInEx.NET.Framework.Launcher.exe. Si vous avez opté pour un raccourci sur le bureau, vous le trouverez avec une icône et un nom plus reconnaissable.
![image](https://i.imgur.com/ZUiFrhf.png)
- Votre jeu devrait se lancer en même temps qu'une console de modloader, qui contiendra des informations de débogage importantes si vous rencontrez des problèmes.
- Le jeu devrait se connecter automatiquement, et tenter de se reconnecter si votre internet ou le serveur se déconnecte, pendant que vous jouez.
### Interagir avec le MultiWorld depuis le jeu
Vous ne pouvez pas envoyer de commandes au serveur ou discuter avec les autres joueurs depuis DLC Quest, car le jeu ne dispose pas d'un moyen approprié pour saisir du texte.
Vous pouvez suivre l'activité du serveur dans votre console BepInEx, car les messages de chat d'Archipelago y seront affichés.
Vous devrez utiliser [Archipelago Text Client] (https://github.com/ArchipelagoMW/Archipelago/releases) si vous voulez envoyer des commandes.

View File

@@ -71,7 +71,7 @@ class FFMQClient(SNIClient):
received = await snes_read(ctx, RECEIVED_DATA[0], RECEIVED_DATA[1])
data = await snes_read(ctx, READ_DATA_START, READ_DATA_END - READ_DATA_START)
check_2 = await snes_read(ctx, 0xF53749, 1)
if check_1 == b'\x00' or check_2 == b'\x00':
if check_1 in (b'\x00', b'\x55') or check_2 in (b'\x00', b'\x55'):
return
def get_range(data_range):

View File

@@ -223,11 +223,6 @@ for item, data in item_table.items():
def create_items(self) -> None:
items = []
starting_weapon = self.multiworld.starting_weapon[self.player].current_key.title().replace("_", " ")
if self.multiworld.progressive_gear[self.player]:
for item_group in prog_map:
if starting_weapon in self.item_name_groups[item_group]:
starting_weapon = prog_map[item_group]
break
self.multiworld.push_precollected(self.create_item(starting_weapon))
self.multiworld.push_precollected(self.create_item("Steel Armor"))
if self.multiworld.sky_coin_mode[self.player] == "start_with":

View File

@@ -2,7 +2,7 @@
## Required Software
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). Make sure to check the box for `SNI Client`
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases).
- Hardware or software capable of loading and playing SNES ROM files
- An emulator capable of connecting to SNI such as:
@@ -19,8 +19,8 @@ The Archipelago community cannot supply you with this.
### Windows Setup
1. During the installation of Archipelago, you will have been asked to install the SNI Client. If you did not do this,
or you are on an older version, you may run the installer again to install the SNI Client.
1. Download and install [Archipelago](<https://github.com/ArchipelagoMW/Archipelago/releases/latest>). **The installer
file is located in the assets section at the bottom of the version information.**
2. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM
files.
1. Extract your emulator's folder to your Desktop, or somewhere you will remember.

View File

@@ -17,19 +17,22 @@ The most recent public release of Archipelago can be found on the GitHub Release
Run the exe file, and after accepting the license agreement you will be asked which components you would like to
install.
The generator allows you to generate multiworld games on your computer. The ROM setups are required if anyone in the
game that you generate wants to play any of those games as they are needed to generate the relevant patch files. If you
do not own the game, uncheck the relevant box. If you gain the game later, the installer can be run again to install and
set up new components.
Archipelago installations are automatically bundled with some programs. These include a launcher, a generator, a
server and some clients.
The server will allow you to host the multiworld on your machine. Hosting on your machine requires forwarding the port
- The launcher lets you quickly access Archipelago's different components and programs. It is found under the name
`ArchipelagoLauncher` and can be found in the main directory of your Archipelago installation.
- The generator allows you to generate multiworld games on your computer. Please refer to the 'Generating a game'
section of this guide for more information about it.
- The server will allow you to host the multiworld on your machine. Hosting on your machine requires forwarding the port
you are hosting on. The default port for Archipelago is `38281`. If you are unsure how to do this there are plenty of
other guides on the internet that will be more suited to your hardware.
The `Clients` are what are used to connect your game to the multiworld. If the game you plan to play is available
here, go ahead and install its client as well. If the game you choose to play is supported by Archipelago but not listed
in the installation, check the setup guide for that game. Installing a client for a ROM based game requires you to have
a legally obtained ROM for that game as well.
- The clients are what are used to connect your game to the multiworld. Some games use a client that is automatically
installed with an Archipelago installation. You can access those clients via the launcher or by navigating
to your Archipelago installation.
## Generating a game
@@ -72,14 +75,18 @@ If you have downloaded the settings, or have created a settings file manually, t
#### On your local installation
To generate a game on your local machine, make sure to install the Archipelago software, and ensure to select the
`Generator` component, as well as the `ROM setup` for any games you will want to play. Navigate to your Archipelago
To generate a game on your local machine, make sure to install the Archipelago software. Navigate to your Archipelago
installation (usually C:\ProgramData\Archipelago), and place the settings file you have either created or downloaded
from the website in the `Players` folder.
Run `ArchipelagoGenerate.exe`, and it will inform you whether the generation was successful or not. If successful, there
will be an output zip in the `output` folder (usually named something like `AP_XXXXX.zip`). This will contain all
relevant information to the session, including the spoiler log, if one was generated.
Run `ArchipelagoGenerate.exe`, or click on `Generate` in the launcher, and it will inform you whether the generation
was successful or not. If successful, there will be an output zip in the `output` folder
(usually named something like `AP_XXXXX.zip`). This will contain all relevant information to the session, including the
spoiler log, if one was generated.
Please note that some games require you to own their ROM files to generate with them as they are needed to generate the
relevant patch files. When you generate with a ROM game for the first time, you will be asked to locate its base ROM file.
This step only needs to be done once.
### Generating a multiplayer game
@@ -97,12 +104,9 @@ player name.
#### On the website
Gather all player YAML files into a single place, and compress them into a zip file. This can be done by pressing
ctrl/cmd + clicking on each file until all are selected, right-clicking one of the files, and clicking
`compress to ZIP file` or `send to > compressed folder`.
Navigate to the [Generate Page](/generate), select the host settings you would like, click on `Upload File`, and
select the newly created zip from the opened window.
Gather all player YAML files into a single place, then navigate to the [Generate Page](/generate). Select the host settings
you would like, click on `Upload File(s)`, and select all player YAML files. The site also accepts `zip` archives containing YAML
files.
After some time, you will be redirected to a seed info page that will display the generated seed, the time it was
created, the number of players, the spoiler (if one was created) and all rooms created from this seed.
@@ -114,8 +118,11 @@ It is possible to generate the multiworld locally, using a local Archipelago ins
Archipelago installation folder (usually C:\ProgramData\Archipelago) and placing each YAML file in the `Players` folder.
If the folder does not exist then it must be created manually. The files here should not be compressed.
After filling the `Players` folder, the `ArchipelagoGenerate.exe` program should be run in order to generate a
multiworld. The output of this process is placed in the `output` folder (usually named something like `AP_XXXXX.zip`).
After filling the `Players` folder, run`ArchipelagoGenerate.exe` or click `Generate` in the launcher. The output of
the generation is placed in the `output` folder (usually named something like `AP_XXXXX.zip`).
Please note that if any player in the game you want to generate plays a game that needs a ROM file to generate, you will
need the corresponding ROM files.
##### Changing local host settings for generation
@@ -123,10 +130,12 @@ Sometimes there are various settings that you may want to change before rolling
auto-release, plando support, or setting a password.
All of these settings, plus other options, may be changed by modifying the `host.yaml` file in the Archipelago
installation folder. The settings chosen here are baked into the `.archipelago` file that gets output with the other
files after generation, so if you are rolling locally, ensure this file is edited to your liking **before** rolling the
seed. This file is overwritten when running the Archipelago Installation software. If you have changed settings in this
file, and would like to retain them, you may rename the file to `options.yaml`.
installation folder. You can quickly access this file by clicking on `Open host.yaml` in the launcher. The settings
chosen here are baked into the `.archipelago` file that gets output with the other files after generation, so if you
are rolling locally, ensure this file is edited to your liking **before** rolling the seed. This file is overwritten
when running the Archipelago Installation software. If you have changed settings in this file, and would like to retain
them, you may rename the file to `options.yaml`.
## Hosting an Archipelago Server

File diff suppressed because one or more lines are too long

View File

@@ -2,7 +2,7 @@ from dataclasses import dataclass
from Options import Choice, Range, Toggle, ItemDict, PerGameCommonOptions, StartInventoryPool
from worlds.kh2 import default_itempool_option
from . import default_itempool_option
class SoraEXP(Range):

View File

@@ -1,7 +1,7 @@
from typing import Dict, Callable, TYPE_CHECKING
from BaseClasses import CollectionState
from .Items import exclusion_item_table, visit_locking_dict, DonaldAbility_Table, GoofyAbility_Table
from .Items import exclusion_item_table, visit_locking_dict, DonaldAbility_Table, GoofyAbility_Table, SupportAbility_Table
from .Locations import exclusion_table, popups_set, Goofy_Checks, Donald_Checks
from .Names import LocationName, ItemName, RegionName
from worlds.generic.Rules import add_rule, forbid_items, add_item_rule
@@ -224,7 +224,7 @@ class KH2WorldRules(KH2Rules):
RegionName.Pl2: lambda state: self.pl_unlocked(state, 2),
RegionName.Ag: lambda state: self.ag_unlocked(state, 1),
RegionName.Ag2: lambda state: self.ag_unlocked(state, 2) and self.kh2_has_all([ItemName.FireElement,ItemName.BlizzardElement,ItemName.ThunderElement],state),
RegionName.Ag2: lambda state: self.ag_unlocked(state, 2) and self.kh2_has_all([ItemName.FireElement, ItemName.BlizzardElement, ItemName.ThunderElement], state),
RegionName.Bc: lambda state: self.bc_unlocked(state, 1),
RegionName.Bc2: lambda state: self.bc_unlocked(state, 2),
@@ -266,6 +266,8 @@ class KH2WorldRules(KH2Rules):
add_item_rule(location, lambda item: item.player == self.player and item.name in GoofyAbility_Table.keys())
elif location.name in Donald_Checks:
add_item_rule(location, lambda item: item.player == self.player and item.name in DonaldAbility_Table.keys())
else:
add_item_rule(location, lambda item: item.player == self.player and item.name in SupportAbility_Table.keys())
def set_kh2_goal(self):
final_xemnas_location = self.multiworld.get_location(LocationName.FinalXemnas, self.player)
@@ -417,7 +419,7 @@ class KH2FightRules(KH2Rules):
RegionName.DataLexaeus: lambda state: self.get_data_lexaeus_rules(state),
RegionName.OldPete: lambda state: self.get_old_pete_rules(),
RegionName.FuturePete: lambda state: self.get_future_pete_rules(state),
RegionName.Terra: lambda state: self.get_terra_rules(state),
RegionName.Terra: lambda state: self.get_terra_rules(state) and state.has(ItemName.ProofofConnection, self.player),
RegionName.DataMarluxia: lambda state: self.get_data_marluxia_rules(state),
RegionName.Barbosa: lambda state: self.get_barbosa_rules(state),
RegionName.GrimReaper1: lambda state: self.get_grim_reaper1_rules(),

View File

@@ -16,6 +16,7 @@ The [player settings page for this game](../player-settings) contains all the op
- Popups
- Get Bonuses
- Form Levels
- Summon Levels
- Sora's Levels
- Keyblade Stats
- Keyblade Abilities
@@ -23,7 +24,7 @@ The [player settings page for this game](../player-settings) contains all the op
<h2 style="text-transform:none";>What Kingdom Hearts 2 items can appear in other players' worlds?</h2>
Every item in the game except for party members' abilities.
Every item in the game except for abilities on weapons.
<h2 style="text-transform:none";>What is The Garden of Assemblage "GoA"?</h2>
@@ -73,6 +74,8 @@ The list of possible locations that can contain a bounty:
- Each of the 13 Data Fights
- Max level (7) for each Drive Form
- Max level (7) of Summons
- Last song of Atlantica
- Sephiroth
- Lingering Will
- Starry Hill
@@ -87,6 +90,7 @@ With the help of Shananas, Num, and ZakTheRobot we have many QoL features such a
- Faster Wardrobe.
- Faster Water Jafar Chase.
- Faster Bulky Vendors
- Carpet Skip.
- Start with Lion Dash.
- Faster Urns.

View File

@@ -7,14 +7,14 @@
<h2 style="text-transform:none";>Required Software:</h2>
`Kingdom Hearts II Final Mix` from the [Epic Games Store](https://store.epicgames.com/en-US/discover/kingdom-hearts)
- Follow this Guide to set up these requirements [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/)<br>
1. `3.0.0 OpenKH Mod Manager with Panacea`<br>
2. `Install mod from KH2FM-Mods-Num/GoA-ROM-Edition`<br>
3. `Setup Lua Backend From the 3.0.0 KH2Randomizer.exe per the setup guide linked above`<br>
1. `3.2.0 OpenKH Mod Manager with Panacea`<br>
2. `Lua Backend from the OpenKH Mod Manager`
3. `Install the mod KH2FM-Mods-Num/GoA-ROM-Edition using OpenKH Mod Manager`<br>
- Needed for Archipelago
1. [`ArchipelagoKH2Client.exe`](https://github.com/ArchipelagoMW/Archipelago/releases)<br>
2. `Install mod from JaredWeakStrike/APCompanion`<br>
3. `Install mod from KH2FM-Mods-equations19/auto-save`<br>
2. `Install the mod from JaredWeakStrike/APCompanion using OpenKH Mod Manager`<br>
3. `Install the mod from KH2FM-Mods-equations19/auto-save using OpenKH Mod Manager`<br>
4. `AP Randomizer Seed`
<h3 style="text-transform:none";>Required: Archipelago Companion Mod</h3>
@@ -68,8 +68,8 @@ Enter `The room's port number` into the top box <b> where the x's are</b> and pr
- Run the game in windows/borderless windowed mode. Fullscreen is stable but the game can crash if you alt-tab out.
- Make sure to save in a different save slot when playing in an async or disconnecting from the server to play a different seed
<h2 style="text-transform:none";>Requirement/logic sheet</h2>
Have any questions on what's in logic? This spreadsheet has the answer [Requirements/logic sheet](https://docs.google.com/spreadsheets/d/1Embae0t7pIrbzvX-NRywk7bTHHEvuFzzQBUUpSUL7Ak/edit?usp=sharing)
<h2 style="text-transform:none";>Logic Sheet</h2>
Have any questions on what's in logic? This spreadsheet made by Bulcon has the answer [Requirements/logic sheet](https://docs.google.com/spreadsheets/d/1nNi8ohEs1fv-sDQQRaP45o6NoRcMlLJsGckBonweDMY/edit?usp=sharing)
<h2 style="text-transform:none";>F.A.Q.</h2>
- Why is my HP/MP continuously increasing without stopping?

View File

@@ -2,7 +2,7 @@
## Required Software
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). Make sure to check the box for `Links Awakening DX`
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases).
- Software capable of loading and playing GBC ROM files
- [RetroArch](https://retroarch.com?page=platforms) 1.10.3 or newer.
- [BizHawk](https://tasvideos.org/BizHawk) 2.8 or newer.
@@ -10,11 +10,12 @@
## Installation Procedures
1. Download and install LinksAwakeningClient from the link above, making sure to install the most recent version.
**The installer file is located in the assets section at the bottom of the version information**.
- During setup, you will be asked to locate your base ROM file. This is your Links Awakening DX ROM file.
1. Download and install [Archipelago](<https://github.com/ArchipelagoMW/Archipelago/releases/latest>). **The installer
file is located in the assets section at the bottom of the version information.**
2. The first time you do local generation or patch your game, you will be asked to locate your base ROM file.
This is your Links Awakening DX ROM file. This only needs to be done once..
2. You should assign your emulator as your default program for launching ROM
3. You should assign your emulator as your default program for launching ROM
files.
1. Extract your emulator's folder to your Desktop, or somewhere you will remember.
2. Right-click on a ROM file and select **Open with...**

View File

@@ -30,8 +30,8 @@ guide: [Basic Multiworld Setup Guide](/tutorial/Archipelago/setup/en)
### Where do I get a config file?
The [Player Settings Page](../player-settings) on the website allows you to easily configure your personal settings
and export a config file from them.
The [Player Settings Page](/games/Landstalker%20-%20The%20Treasures%20of%20King%20Nole/player-settings) on the website allows
you to easily configure your personal settings
## How-to-play

View File

@@ -2,7 +2,7 @@
## Required Software
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). Make sure to check the box for `SNI Client - Lufia II Ancient Cave Patch Setup`
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases).
- Hardware or software capable of loading and playing SNES ROM files
- An emulator capable of connecting to SNI
([snes9x rr](https://github.com/gocha/snes9x-rr/releases),
@@ -14,11 +14,12 @@ modded SNES minis are currently not supported by SNI**
## Installation Procedures
1. Download and install SNIClient from the link above, making sure to install the most recent version.
**The installer file is located in the assets section at the bottom of the version information**.
- During setup, you will be asked to locate your base ROM file. This is your American Lufia II - Rise of the Sinistrals ROM file.
1. Download and install [Archipelago](<https://github.com/ArchipelagoMW/Archipelago/releases/latest>). **The installer
file is located in the assets section at the bottom of the version information.**
2. The first time you do local generation or patch your game, you will be asked to locate your base ROM file.
This is your American Lufia II - Rise of the Sinistrals ROM file. This only needs to be done once.
2. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM
3. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM
files.
1. Extract your emulator's folder to your Desktop, or somewhere you will remember.
2. Right-click on a ROM file and select **Open with...**

View File

@@ -5,7 +5,6 @@
- Minecraft Java Edition from
the [Minecraft Java Edition Store Page](https://www.minecraft.net/en-us/store/minecraft-java-edition)
- Archipelago from the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases)
- (select `Minecraft Client` during installation.)
## Configuring your YAML file

View File

@@ -10,8 +10,7 @@ As we are using Bizhawk, this guide is only applicable to Windows and Linux syst
- Version 2.7.0 and later are supported.
- Detailed installation instructions for Bizhawk can be found at the above link.
- Windows users must run the prereq installer first, which can also be found at the above link.
- The built-in Archipelago client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases)
(select `MegaMan Battle Network 3 Client` during installation).
- The built-in Archipelago client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases).
- A US MegaMan Battle Network 3 Blue Rom. If you have the [MegaMan Battle Network Legacy Collection Vol. 1](https://store.steampowered.com/app/1798010/Mega_Man_Battle_Network_Legacy_Collection_Vol_1/)
on Steam, you can obtain a copy of this ROM from the game's files, see instructions below.

View File

@@ -34,6 +34,7 @@ class MuseDashCollections:
"Rush-Hour",
"Find this Month's Featured Playlist",
"PeroPero in the Universe",
"umpopoff"
]
album_items: Dict[str, AlbumData] = {}
@@ -81,11 +82,22 @@ class MuseDashCollections:
steamer_mode = sections[3] == "True"
if song_name in self.DIFF_OVERRIDES:
# Note: These difficulties may not actually be representative of these songs.
# The game does not provide these difficulties so they have to be filled in.
diff_of_easy = 4
diff_of_hard = 7
diff_of_master = 10
# These songs use non-standard difficulty values. Which are being overriden with standard values.
# But also avoid filling any missing difficulties (i.e. 0s) with a difficulty value.
if sections[4] != '0':
diff_of_easy = 4
else:
diff_of_easy = None
if sections[5] != '0':
diff_of_hard = 7
else:
diff_of_hard = None
if sections[6] != '0':
diff_of_master = 10
else:
diff_of_master = None
else:
diff_of_easy = self.parse_song_difficulty(sections[4])
diff_of_hard = self.parse_song_difficulty(sections[5])

View File

@@ -119,7 +119,7 @@ Prestige and Vestige|56-4|Give Up TREATMENT Vol.11|True|6|8|11|
Tiny Fate|56-5|Give Up TREATMENT Vol.11|False|7|9|11|
Tsuki ni Murakumo Hana ni Kaze|55-0|Touhou Mugakudan -2-|False|3|5|7|
Patchouli's - Best Hit GSK|55-1|Touhou Mugakudan -2-|False|3|5|8|
Monosugoi Space Shuttle de Koishi ga Monosugoi uta|55-2|Touhou Mugakudan -2-|False|3|5|7|
Monosugoi Space Shuttle de Koishi ga Monosugoi uta|55-2|Touhou Mugakudan -2-|False|3|5|7|11
Kakoinaki Yo wa Ichigo no Tsukikage|55-3|Touhou Mugakudan -2-|False|3|6|8|
Psychedelic Kizakura Doumei|55-4|Touhou Mugakudan -2-|False|4|7|10|
Mischievous Sensation|55-5|Touhou Mugakudan -2-|False|5|7|9|
@@ -501,4 +501,12 @@ slic.hertz|68-1|Gambler's Tricks|True|5|7|9|
Fuzzy-Navel|68-2|Gambler's Tricks|True|6|8|10|11
Swing Edge|68-3|Gambler's Tricks|True|4|8|10|
Twisted Escape|68-4|Gambler's Tricks|True|5|8|10|11
Swing Sweet Twee Dance|68-5|Gambler's Tricks|False|4|7|10|
Swing Sweet Twee Dance|68-5|Gambler's Tricks|False|4|7|10|
Sanyousei SAY YA!!!|43-42|MD Plus Project|False|4|6|8|
YUKEMURI TAMAONSEN II|43-43|MD Plus Project|False|3|6|9|
Samayoi no mei Amatsu|69-0|Touhou Mugakudan -3-|False|4|6|9|
INTERNET SURVIVOR|69-1|Touhou Mugakudan -3-|False|5|8|10|
Shuki*RaiRai|69-2|Touhou Mugakudan -3-|False|5|7|9|
HELLOHELL|69-3|Touhou Mugakudan -3-|False|4|7|10|
Calamity Fortune|69-4|Touhou Mugakudan -3-|True|6|8|10|11
Tsurupettan|69-5|Touhou Mugakudan -3-|True|2|5|8|

View File

@@ -36,7 +36,7 @@ class AdditionalSongs(Range):
- The final song count may be lower due to other settings.
"""
range_start = 15
range_end = 500 # Note will probably not reach this high if any other settings are done.
range_end = 508 # Note will probably not reach this high if any other settings are done.
default = 40
display_name = "Additional Song Count"

View File

@@ -328,5 +328,6 @@ class MuseDashWorld(World):
"victoryLocation": self.victory_song_name,
"deathLink": self.options.death_link.value,
"musicSheetWinCount": self.get_music_sheet_win_count(),
"gradeNeeded": self.options.grade_needed.value
"gradeNeeded": self.options.grade_needed.value,
"hasFiller": True,
}

View File

@@ -66,5 +66,11 @@ class DifficultyRanges(MuseDashTestBase):
for song_name in muse_dash_world.md_collection.DIFF_OVERRIDES:
song = muse_dash_world.md_collection.song_items[song_name]
self.assertTrue(song.easy is not None and song.hard is not None and song.master is not None,
# umpopoff is a one time weird song. Its currently the only song in the game
# with non-standard difficulties and also doesn't have 3 or more difficulties.
if song_name == 'umpopoff':
self.assertTrue(song.easy is None and song.hard is not None and song.master is None,
f"Song '{song_name}' difficulty not set when it should be.")
else:
self.assertTrue(song.easy is not None and song.hard is not None and song.master is not None,
f"Song '{song_name}' difficulty not set when it should be.")

View File

@@ -10,8 +10,7 @@ As we are using BizHawk, this guide is only applicable to Windows and Linux syst
- Version 2.3.1 and later are supported. Version 2.7 is recommended for stability.
- Detailed installation instructions for BizHawk can be found at the above link.
- Windows users must run the prereq installer first, which can also be found at the above link.
- The built-in Archipelago client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases)
(select `Ocarina of Time Client` during installation).
- The built-in Archipelago client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases).
- An Ocarina of Time v1.0 ROM.
## Configuring BizHawk

View File

@@ -353,7 +353,9 @@ class PokemonRedBlueWorld(World):
location.show_in_spoiler = False
def intervene(move, test_state):
if self.multiworld.randomize_wild_pokemon[self.player]:
move_bit = pow(2, poke_data.hm_moves.index(move) + 2)
viable_mons = [mon for mon in self.local_poke_data if self.local_poke_data[mon]["tms"][6] & move_bit]
if self.multiworld.randomize_wild_pokemon[self.player] and viable_mons:
accessible_slots = [loc for loc in self.multiworld.get_reachable_locations(test_state, self.player) if
loc.type == "Wild Encounter"]
@@ -363,8 +365,6 @@ class PokemonRedBlueWorld(World):
zones.add(loc.name.split(" - ")[0])
return len(zones)
move_bit = pow(2, poke_data.hm_moves.index(move) + 2)
viable_mons = [mon for mon in self.local_poke_data if self.local_poke_data[mon]["tms"][6] & move_bit]
placed_mons = [slot.item.name for slot in accessible_slots]
if self.multiworld.area_1_to_1_mapping[self.player]:

View File

@@ -3,7 +3,6 @@ from __future__ import annotations
import asyncio
import copy
import ctypes
import json
import logging
import multiprocessing
import os.path
@@ -15,6 +14,7 @@ import queue
import zipfile
import io
import random
import concurrent.futures
from pathlib import Path
# CommonClient import first to trigger ModuleUpdater
@@ -42,6 +42,7 @@ import colorama
from NetUtils import ClientStatus, NetworkItem, RawJSONtoTextParser, JSONtoTextParser, JSONMessagePart
from MultiServer import mark_raw
pool = concurrent.futures.ThreadPoolExecutor(1)
loop = asyncio.get_event_loop_policy().new_event_loop()
nest_asyncio.apply(loop)
max_bonus: int = 13
@@ -210,6 +211,11 @@ class StarcraftClientProcessor(ClientCommandProcessor):
def _cmd_download_data(self) -> bool:
"""Download the most recent release of the necessary files for playing SC2 with
Archipelago. Will overwrite existing files."""
pool.submit(self._download_data)
return True
@staticmethod
def _download_data() -> bool:
if "SC2PATH" not in os.environ:
check_game_install_path()
@@ -220,7 +226,7 @@ class StarcraftClientProcessor(ClientCommandProcessor):
metadata = None
tempzip, metadata = download_latest_release_zip(DATA_REPO_OWNER, DATA_REPO_NAME, DATA_API_VERSION,
metadata=metadata, force_download=True)
metadata=metadata, force_download=True)
if tempzip != '':
try:

View File

@@ -151,14 +151,14 @@ def get_rules_lookup(player: int):
"Puzzle Solved Maze Door": lambda state: state.can_reach("Projector Room", "Region", player),
"Puzzle Solved Theater Door": lambda state: state.can_reach("Underground Lake", "Region", player),
"Puzzle Solved Columns of RA": lambda state: state.can_reach("Underground Lake", "Region", player),
"Final Riddle: Guillotine Dropped": lambda state: state.can_reach("Underground Lake", "Region", player)
"Final Riddle: Guillotine Dropped": lambda state: (beths_body_available(state, player) and state.can_reach("Underground Lake", "Region", player))
},
"elevators": {
"Puzzle Solved Underground Elevator": lambda state: ((state.can_reach("Underground Lake", "Region", player) or state.can_reach("Office", "Region", player)
and state.has("Key for Office Elevator", player))),
"Puzzle Solved Office Elevator": lambda state: ((state.can_reach("Underground Lake", "Region", player) or state.can_reach("Office", "Region", player))
and state.has("Key for Office Elevator", player)),
"Puzzle Solved Bedroom Elevator": lambda state: (state.can_reach("Office", "Region", player) and state.has_all({"Key for Bedroom Elevator","Crawling"}, player)),
"Puzzle Solved Three Floor Elevator": lambda state: (((state.can_reach("Maintenance Tunnels", "Region", player) or state.can_reach("Blue Maze", "Region", player))
and state.has("Key for Three Floor Elevator", player)))
"Puzzle Solved Three Floor Elevator": lambda state: ((state.can_reach("Maintenance Tunnels", "Region", player) or state.can_reach("Blue Maze", "Region", player))
and state.has("Key for Three Floor Elevator", player))
},
"lightning": {
"Ixupi Captured Lightning": lambda state: lightning_capturable(state, player)

View File

@@ -42,7 +42,7 @@
"Information Plaque: Aliens (UFO)"
],
"elevators": [
"Puzzle Solved Underground Elevator",
"Puzzle Solved Office Elevator",
"Puzzle Solved Bedroom Elevator",
"Puzzle Solved Three Floor Elevator"
],

View File

@@ -110,7 +110,7 @@
"Information Plaque: Astronomical Construction (UFO)",
"Information Plaque: Guillotine (Torture)",
"Information Plaque: Aliens (UFO)",
"Puzzle Solved Underground Elevator",
"Puzzle Solved Office Elevator",
"Puzzle Solved Bedroom Elevator",
"Puzzle Solved Three Floor Elevator",
"Ixupi Captured Lightning"
@@ -129,7 +129,7 @@
"Ixupi Captured Sand",
"Ixupi Captured Metal",
"Ixupi Captured Lightning",
"Puzzle Solved Underground Elevator",
"Puzzle Solved Office Elevator",
"Puzzle Solved Three Floor Elevator",
"Puzzle Hint Found: Combo Lock in Mailbox",
"Puzzle Hint Found: Orange Symbol",

View File

@@ -2,7 +2,7 @@
## Required Software
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). Make sure to check the box for `SNI Client - Super Metroid Patch Setup`
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases).
- Hardware or software capable of loading and playing SNES ROM files
@@ -18,9 +18,10 @@
### Windows Setup
1. During the installation of Archipelago, you will have been asked to install the SNI Client. If you did not do this,
or you are on an older version, you may run the installer again to install the SNI Client.
2. During setup, you will be asked to locate your base ROM file. This is your Super Metroid ROM file.
1. Download and install [Archipelago](<https://github.com/ArchipelagoMW/Archipelago/releases/latest>). **The installer
file is located in the assets section at the bottom of the version information.**
2. The first time you do local generation or patch your game, you will be asked to locate your base ROM file.
This is your Super Metroid ROM file. This only needs to be done once.
3. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM
files.
1. Extract your emulator's folder to your Desktop, or somewhere you will remember.

View File

@@ -2,8 +2,7 @@
## Required Software
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). Make sure to check the box for `SNI Client - Super Mario World Patch Setup`
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases).
- Hardware or software capable of loading and playing SNES ROM files
- An emulator capable of connecting to SNI such as:
@@ -23,9 +22,10 @@
### Windows Setup
1. During the installation of Archipelago, you will have been asked to install the SNI Client. If you did not do this,
or you are on an older version, you may run the installer again to install the SNI Client.
2. During setup, you will be asked to locate your base ROM file. This is your Super Mario World ROM file.
1. Download and install [Archipelago](<https://github.com/ArchipelagoMW/Archipelago/releases/latest>). **The installer
file is located in the assets section at the bottom of the version information.**
2. The first time you do local generation or patch your game, you will be asked to locate your base ROM file.
This is your Super Mario World ROM file. This only needs to be done once.
3. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM
files.
1. Extract your emulator's folder to your Desktop, or somewhere you will remember.

View File

@@ -40,6 +40,7 @@ class SMZ3SNIClient(SNIClient):
if rom_name is None or rom_name == bytes([0] * ROMNAME_SIZE) or rom_name[:3] != b"ZSM":
return False
ctx.smz3_new_message_queue = rom_name[7] in b"1234567890"
ctx.game = self.game
ctx.items_handling = 0b101 # local items and remote start inventory
@@ -53,6 +54,22 @@ class SMZ3SNIClient(SNIClient):
if ctx.server is None or ctx.slot is None:
# not successfully connected to a multiworld server, cannot process the game sending items
return
send_progress_addr_ptr_offset = 0x680
send_progress_size = 8
send_progress_message_byte_offset = 4
send_progress_addr_table_offset = 0x700
recv_progress_addr_ptr_offset = 0x600
recv_progress_size = 4
recv_progress_addr_table_offset = 0x602
if ctx.smz3_new_message_queue:
send_progress_addr_ptr_offset = 0xD3C
send_progress_size = 2
send_progress_message_byte_offset = 0
send_progress_addr_table_offset = 0xDA0
recv_progress_addr_ptr_offset = 0xD36
recv_progress_size = 2
recv_progress_addr_table_offset = 0xD38
currentGame = await snes_read(ctx, SRAM_START + 0x33FE, 2)
if (currentGame is not None):
@@ -69,7 +86,7 @@ class SMZ3SNIClient(SNIClient):
ctx.finished_game = True
return
data = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0xD3C, 4)
data = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + send_progress_addr_ptr_offset, 4)
if data is None:
return
@@ -77,14 +94,14 @@ class SMZ3SNIClient(SNIClient):
recv_item = data[2] | (data[3] << 8)
while (recv_index < recv_item):
item_address = recv_index * 2
message = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0xDA0 + item_address, 2)
is_z3_item = ((message[1] & 0x80) != 0)
masked_part = (message[1] & 0x7F) if is_z3_item else message[1]
item_index = ((message[0] | (masked_part << 8)) >> 3) + (256 if is_z3_item else 0)
item_address = recv_index * send_progress_size
message = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + send_progress_addr_table_offset + item_address, send_progress_size)
is_z3_item = ((message[send_progress_message_byte_offset+1] & 0x80) != 0)
masked_part = (message[send_progress_message_byte_offset+1] & 0x7F) if is_z3_item else message[send_progress_message_byte_offset+1]
item_index = ((message[send_progress_message_byte_offset] | (masked_part << 8)) >> 3) + (256 if is_z3_item else 0)
recv_index += 1
snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + 0xD3C, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + send_progress_addr_ptr_offset, bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
from .TotalSMZ3.Location import locations_start_id
from . import convertLocSMZ3IDToAPID
@@ -95,7 +112,7 @@ class SMZ3SNIClient(SNIClient):
snes_logger.info(f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})')
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [location_id]}])
data = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + 0xD36, 4)
data = await snes_read(ctx, SMZ3_RECV_PROGRESS_ADDR + recv_progress_addr_ptr_offset, 4)
if data is None:
return
@@ -107,9 +124,12 @@ class SMZ3SNIClient(SNIClient):
item_id = item.item - items_start_id
player_id = item.player if item.player < SMZ3_ROM_PLAYER_LIMIT else 0
snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + item_out_ptr * 2, bytes([player_id, item_id]))
snes_buffered_write(ctx,
SMZ3_RECV_PROGRESS_ADDR + item_out_ptr * recv_progress_size,
bytes([player_id, item_id]) if ctx.smz3_new_message_queue else
bytes([player_id & 0xFF, (player_id >> 8) & 0xFF, item_id & 0xFF, (item_id >> 8) & 0xFF]))
item_out_ptr += 1
snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + 0xD38, bytes([item_out_ptr & 0xFF, (item_out_ptr >> 8) & 0xFF]))
snes_buffered_write(ctx, SMZ3_RECV_PROGRESS_ADDR + recv_progress_addr_table_offset, bytes([item_out_ptr & 0xFF, (item_out_ptr >> 8) & 0xFF]))
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
color(ctx.item_names[item.item], 'red', 'bold'), color(ctx.player_names[item.player], 'yellow'),
ctx.location_names[item.location], item_out_ptr, len(ctx.items_received)))

View File

@@ -616,7 +616,8 @@ class Patch:
"H" if self.myWorld.Config.SMLogic == Config.SMLogic.Hard else \
"X"
self.title = f"ZSM{Patch.Major}{Patch.Minor}{Patch.Patch}{z3Glitch}{smGlitch}{self.myWorld.Id}{self.seed:08x}".ljust(21)[:21]
from Utils import __version__
self.title = f"ZSM{Patch.Major}{Patch.Minor}{Patch.Patch}{__version__.replace('.', '')[0:3]}{z3Glitch}{smGlitch}{self.myWorld.Id}{self.seed:08x}".ljust(21)[:21]
self.patches.append((Snes(0x00FFC0), bytearray(self.title, 'utf8')))
self.patches.append((Snes(0x80FFC0), bytearray(self.title, 'utf8')))

View File

@@ -4,8 +4,7 @@
- One of the client programs:
- [SNIClient](https://github.com/ArchipelagoMW/Archipelago/releases), included with the main
Archipelago install. Make sure to check the box for `SNI Client - Super Metroid Patch Setup` and
`SNI Client - A Link to the Past Patch Setup`
Archipelago install.
- Hardware or software capable of loading and playing SNES ROM files
- An emulator capable of connecting to SNI such as:
- snes9x-rr from: [snes9x rr](https://github.com/gocha/snes9x-rr/releases),
@@ -20,9 +19,10 @@
### Windows Setup
1. During the installation of Archipelago, you will have been asked to install the SNI Client. If you did not do this,
or you are on an older version, you may run the installer again to install the SNI Client.
2. During setup, you will be asked to locate your base ROM files. This is your Super Metroid and Zelda3 ROM files.
1. Download and install [Archipelago](<https://github.com/ArchipelagoMW/Archipelago/releases/latest>). **The installer
file is located in the assets section at the bottom of the version information.**
2. The first time you do local generation or patch your game, you will be asked to locate your base ROM file.
This is your Super Metroid and Zelda3 ROM files. This only needs to be done once.
3. If you are using an emulator, you should assign your Lua capable emulator as your default program for launching ROM
files.
1. Extract your emulator's folder to your Desktop, or somewhere you will remember.

View File

@@ -1,70 +0,0 @@
from typing import Protocol, Set
from BaseClasses import MultiWorld
from worlds.AutoWorld import LogicMixin
from . import pyevermizer
from .Options import EnergyCore, OutOfBounds, SequenceBreaks
# TODO: Options may preset certain progress steps (i.e. P_ROCK_SKIP), set in generate_early?
# TODO: resolve/flatten/expand rules to get rid of recursion below where possible
# Logic.rules are all rules including locations, excluding those with no progress (i.e. locations that only drop items)
rules = [rule for rule in pyevermizer.get_logic() if len(rule.provides) > 0]
# Logic.items are all items and extra items excluding non-progression items and duplicates
item_names: Set[str] = set()
items = [item for item in filter(lambda item: item.progression, pyevermizer.get_items() + pyevermizer.get_extra_items())
if item.name not in item_names and not item_names.add(item.name)]
class LogicProtocol(Protocol):
def has(self, name: str, player: int) -> bool: ...
def count(self, name: str, player: int) -> int: ...
def soe_has(self, progress: int, world: MultiWorld, player: int, count: int) -> bool: ...
def _soe_count(self, progress: int, world: MultiWorld, player: int, max_count: int) -> int: ...
# when this module is loaded, this mixin will extend BaseClasses.CollectionState
class SecretOfEvermoreLogic(LogicMixin):
def _soe_count(self: LogicProtocol, progress: int, world: MultiWorld, player: int, max_count: int = 0) -> int:
"""
Returns reached count of one of evermizer's progress steps based on collected items.
i.e. returns 0-3 for P_DE based on items providing CHECK_BOSS,DIAMOND_EYE_DROP
"""
n = 0
for item in items:
for pvd in item.provides:
if pvd[1] == progress:
if self.has(item.name, player):
n += self.count(item.name, player) * pvd[0]
if n >= max_count > 0:
return n
for rule in rules:
for pvd in rule.provides:
if pvd[1] == progress and pvd[0] > 0:
has = True
for req in rule.requires:
if not self.soe_has(req[1], world, player, req[0]):
has = False
break
if has:
n += pvd[0]
if n >= max_count > 0:
return n
return n
def soe_has(self: LogicProtocol, progress: int, world: MultiWorld, player: int, count: int = 1) -> bool:
"""
Returns True if count of one of evermizer's progress steps is reached based on collected items. i.e. 2 * P_DE
"""
if progress == pyevermizer.P_ENERGY_CORE: # logic is shared between worlds, so we override in the call
w = world.worlds[player]
if w.energy_core == EnergyCore.option_fragments:
progress = pyevermizer.P_CORE_FRAGMENT
count = w.required_fragments
elif progress == pyevermizer.P_ALLOW_OOB:
if world.worlds[player].out_of_bounds == OutOfBounds.option_logic:
return True
elif progress == pyevermizer.P_ALLOW_SEQUENCE_BREAKS:
if world.worlds[player].sequence_breaks == SequenceBreaks.option_logic:
return True
return self._soe_count(progress, world, player, count) >= count

View File

@@ -4,18 +4,23 @@ import os.path
import threading
import typing
# from . import pyevermizer # as part of the source tree
import pyevermizer # from package
import settings
from BaseClasses import Item, ItemClassification, Location, LocationProgressType, Region, Tutorial
from Utils import output_path
from worlds.AutoWorld import WebWorld, World
from worlds.generic.Rules import add_item_rule, set_rule
from BaseClasses import Entrance, Item, ItemClassification, Location, LocationProgressType, Region, Tutorial
from Utils import output_path
from .logic import SoEPlayerLogic
from .options import Difficulty, EnergyCore, SoEOptions
from .patch import SoEDeltaPatch, get_base_rom_path
import pyevermizer # from package
# from . import pyevermizer # as part of the source tree
if typing.TYPE_CHECKING:
from BaseClasses import MultiWorld, CollectionState
__all__ = ["pyevermizer", "SoEWorld"]
from . import Logic # load logic mixin
from .Options import soe_options, Difficulty, EnergyCore, RequiredFragments, AvailableFragments
from .Patch import SoEDeltaPatch, get_base_rom_path
"""
In evermizer:
@@ -24,17 +29,17 @@ Items are uniquely defined by a pair of (type, id).
For most items this is their vanilla location (i.e. CHECK_GOURD, number).
Items have `provides`, which give the actual progression
instead of providing multiple events per item, we iterate through them in Logic.py
instead of providing multiple events per item, we iterate through them in logic.py
e.g. Found any weapon
Locations have `requires` and `provides`.
Requirements have to be converted to (access) rules for AP
e.g. Chest locked behind having a weapon
Provides could be events, but instead we iterate through the entire logic in Logic.py
Provides could be events, but instead we iterate through the entire logic in logic.py
e.g. NPC available after fighting a Boss
Rules are special locations that don't have a physical location
instead of implementing virtual locations and virtual items, we simply use them in Logic.py
instead of implementing virtual locations and virtual items, we simply use them in logic.py
e.g. 2DEs+Wheel+Gauge = Rocket
Rules and Locations live on the same logic tree returned by pyevermizer.get_logic()
@@ -84,8 +89,8 @@ _other_items = (
)
def _match_item_name(item, substr: str) -> bool:
sub = item.name.split(' ', 1)[1] if item.name[0].isdigit() else item.name
def _match_item_name(item: pyevermizer.Item, substr: str) -> bool:
sub: str = item.name.split(' ', 1)[1] if item.name[0].isdigit() else item.name
return sub == substr or sub == substr+'s'
@@ -156,10 +161,11 @@ class SoESettings(settings.Group):
class SoEWorld(World):
"""
Secret of Evermore is a SNES action RPG. You learn alchemy spells, fight bosses and gather rocket parts to visit a
space station where the final boss must be defeated.
space station where the final boss must be defeated.
"""
game: str = "Secret of Evermore"
option_definitions = soe_options
game: typing.ClassVar[str] = "Secret of Evermore"
options_dataclass = SoEOptions
options: SoEOptions
settings: typing.ClassVar[SoESettings]
topology_present = False
data_version = 4
@@ -170,31 +176,21 @@ class SoEWorld(World):
location_name_to_id, location_id_to_raw = _get_location_mapping()
item_name_groups = _get_item_grouping()
trap_types = [name[12:] for name in option_definitions if name.startswith('trap_chance_')]
logic: SoEPlayerLogic
evermizer_seed: int
connect_name: str
energy_core: int
sequence_breaks: int
out_of_bounds: int
available_fragments: int
required_fragments: int
_halls_ne_chest_names: typing.List[str] = [loc.name for loc in _locations if 'Halls NE' in loc.name]
def __init__(self, *args, **kwargs):
def __init__(self, multiworld: "MultiWorld", player: int):
self.connect_name_available_event = threading.Event()
super(SoEWorld, self).__init__(*args, **kwargs)
super(SoEWorld, self).__init__(multiworld, player)
def generate_early(self) -> None:
# store option values that change logic
self.energy_core = self.multiworld.energy_core[self.player].value
self.sequence_breaks = self.multiworld.sequence_breaks[self.player].value
self.out_of_bounds = self.multiworld.out_of_bounds[self.player].value
self.required_fragments = self.multiworld.required_fragments[self.player].value
if self.required_fragments > self.multiworld.available_fragments[self.player].value:
self.multiworld.available_fragments[self.player].value = self.required_fragments
self.available_fragments = self.multiworld.available_fragments[self.player].value
# create logic from options
if self.options.required_fragments.value > self.options.available_fragments.value:
self.options.available_fragments.value = self.options.required_fragments.value
self.logic = SoEPlayerLogic(self.player, self.options)
def create_event(self, event: str) -> Item:
return SoEItem(event, ItemClassification.progression, None, self.player)
@@ -214,20 +210,20 @@ class SoEWorld(World):
return SoEItem(item.name, classification, self.item_name_to_id[item.name], self.player)
@classmethod
def stage_assert_generate(cls, multiworld):
def stage_assert_generate(cls, _: "MultiWorld") -> None:
rom_file = get_base_rom_path()
if not os.path.exists(rom_file):
raise FileNotFoundError(rom_file)
def create_regions(self):
def create_regions(self) -> None:
# exclude 'hidden' on easy
max_difficulty = 1 if self.multiworld.difficulty[self.player] == Difficulty.option_easy else 256
max_difficulty = 1 if self.options.difficulty == Difficulty.option_easy else 256
# TODO: generate *some* regions from locations' requirements?
menu = Region('Menu', self.player, self.multiworld)
self.multiworld.regions += [menu]
def get_sphere_index(evermizer_loc):
def get_sphere_index(evermizer_loc: pyevermizer.Location) -> int:
"""Returns 0, 1 or 2 for locations in spheres 1, 2, 3+"""
if len(evermizer_loc.requires) == 1 and evermizer_loc.requires[0][1] != pyevermizer.P_WEAPON:
return 2
@@ -252,18 +248,18 @@ class SoEWorld(World):
# mark some as excluded based on numbers above
for trash_sphere, fills in trash_fills.items():
for typ, counts in fills.items():
count = counts[self.multiworld.difficulty[self.player].value]
for location in self.multiworld.random.sample(spheres[trash_sphere][typ], count):
count = counts[self.options.difficulty.value]
for location in self.random.sample(spheres[trash_sphere][typ], count):
assert location.name != "Energy Core #285", "Error in sphere generation"
location.progress_type = LocationProgressType.EXCLUDED
def sphere1_blocked_items_rule(item):
def sphere1_blocked_items_rule(item: pyevermizer.Item) -> bool:
if isinstance(item, SoEItem):
# disable certain items in sphere 1
if item.name in {"Gauge", "Wheel"}:
return False
# and some more for non-easy, non-mystery
if self.multiworld.difficulty[item.player] not in (Difficulty.option_easy, Difficulty.option_mystery):
if self.options.difficulty not in (Difficulty.option_easy, Difficulty.option_mystery):
if item.name in {"Laser Lance", "Atom Smasher", "Diamond Eye"}:
return False
return True
@@ -273,13 +269,13 @@ class SoEWorld(World):
add_item_rule(location, sphere1_blocked_items_rule)
# make some logically late(r) bosses priority locations to increase complexity
if self.multiworld.difficulty[self.player] == Difficulty.option_mystery:
late_count = self.multiworld.random.randint(0, 2)
if self.options.difficulty == Difficulty.option_mystery:
late_count = self.random.randint(0, 2)
else:
late_count = self.multiworld.difficulty[self.player].value
late_count = self.options.difficulty.value
late_bosses = ("Tiny", "Aquagoth", "Megataur", "Rimsala",
"Mungola", "Lightning Storm", "Magmar", "Volcano Viper")
late_locations = self.multiworld.random.sample(late_bosses, late_count)
late_locations = self.random.sample(late_bosses, late_count)
# add locations to the world
for sphere in spheres.values():
@@ -293,17 +289,17 @@ class SoEWorld(World):
menu.connect(ingame, "New Game")
self.multiworld.regions += [ingame]
def create_items(self):
def create_items(self) -> None:
# add regular items to the pool
exclusions: typing.List[str] = []
if self.energy_core != EnergyCore.option_shuffle:
if self.options.energy_core != EnergyCore.option_shuffle:
exclusions.append("Energy Core") # will be placed in generate_basic or replaced by a fragment below
items = list(map(lambda item: self.create_item(item), (item for item in _items if item.name not in exclusions)))
# remove one pair of wings that will be placed in generate_basic
items.remove(self.create_item("Wings"))
def is_ingredient(item):
def is_ingredient(item: pyevermizer.Item) -> bool:
for ingredient in _ingredients:
if _match_item_name(item, ingredient):
return True
@@ -311,84 +307,72 @@ class SoEWorld(World):
# add energy core fragments to the pool
ingredients = [n for n, item in enumerate(items) if is_ingredient(item)]
if self.energy_core == EnergyCore.option_fragments:
if self.options.energy_core == EnergyCore.option_fragments:
items.append(self.create_item("Energy Core Fragment")) # replaces the vanilla energy core
for _ in range(self.available_fragments - 1):
for _ in range(self.options.available_fragments - 1):
if len(ingredients) < 1:
break # out of ingredients to replace
r = self.multiworld.random.choice(ingredients)
r = self.random.choice(ingredients)
ingredients.remove(r)
items[r] = self.create_item("Energy Core Fragment")
# add traps to the pool
trap_count = self.multiworld.trap_count[self.player].value
trap_chances = {}
trap_names = {}
trap_count = self.options.trap_count.value
trap_names: typing.List[str] = []
trap_weights: typing.List[int] = []
if trap_count > 0:
for trap_type in self.trap_types:
trap_option = getattr(self.multiworld, f'trap_chance_{trap_type}')[self.player]
trap_chances[trap_type] = trap_option.value
trap_names[trap_type] = trap_option.item_name
trap_chances_total = sum(trap_chances.values())
if trap_chances_total == 0:
for trap_type in trap_chances:
trap_chances[trap_type] = 1
trap_chances_total = len(trap_chances)
for trap_option in self.options.trap_chances:
trap_names.append(trap_option.item_name)
trap_weights.append(trap_option.value)
if sum(trap_weights) == 0:
trap_weights = [1 for _ in trap_weights]
def create_trap() -> Item:
v = self.multiworld.random.randrange(trap_chances_total)
for t, c in trap_chances.items():
if v < c:
return self.create_item(trap_names[t])
v -= c
assert False, "Bug in create_trap"
return self.create_item(self.random.choices(trap_names, trap_weights)[0])
for _ in range(trap_count):
if len(ingredients) < 1:
break # out of ingredients to replace
r = self.multiworld.random.choice(ingredients)
r = self.random.choice(ingredients)
ingredients.remove(r)
items[r] = create_trap()
self.multiworld.itempool += items
def set_rules(self):
def set_rules(self) -> None:
self.multiworld.completion_condition[self.player] = lambda state: state.has('Victory', self.player)
# set Done from goal option once we have multiple goals
set_rule(self.multiworld.get_location('Done', self.player),
lambda state: state.soe_has(pyevermizer.P_FINAL_BOSS, self.multiworld, self.player))
lambda state: self.logic.has(state, pyevermizer.P_FINAL_BOSS))
set_rule(self.multiworld.get_entrance('New Game', self.player), lambda state: True)
for loc in _locations:
location = self.multiworld.get_location(loc.name, self.player)
set_rule(location, self.make_rule(loc.requires))
def make_rule(self, requires: typing.List[typing.Tuple[int, int]]) -> typing.Callable[[typing.Any], bool]:
def rule(state) -> bool:
def rule(state: "CollectionState") -> bool:
for count, progress in requires:
if not state.soe_has(progress, self.multiworld, self.player, count):
if not self.logic.has(state, progress, count):
return False
return True
return rule
def make_item_type_limit_rule(self, item_type: int):
return lambda item: item.player != self.player or self.item_id_to_raw[item.code].type == item_type
def generate_basic(self):
def generate_basic(self) -> None:
# place Victory event
self.multiworld.get_location('Done', self.player).place_locked_item(self.create_event('Victory'))
# place wings in halls NE to avoid softlock
wings_location = self.multiworld.random.choice(self._halls_ne_chest_names)
wings_location = self.random.choice(self._halls_ne_chest_names)
wings_item = self.create_item('Wings')
self.multiworld.get_location(wings_location, self.player).place_locked_item(wings_item)
# place energy core at vanilla location for vanilla mode
if self.energy_core == EnergyCore.option_vanilla:
if self.options.energy_core == EnergyCore.option_vanilla:
energy_core = self.create_item('Energy Core')
self.multiworld.get_location('Energy Core #285', self.player).place_locked_item(energy_core)
# generate stuff for later
self.evermizer_seed = self.multiworld.random.randint(0, 2 ** 16 - 1) # TODO: make this an option for "full" plando?
self.evermizer_seed = self.random.randint(0, 2 ** 16 - 1) # TODO: make this an option for "full" plando?
def generate_output(self, output_directory: str):
def generate_output(self, output_directory: str) -> None:
player_name = self.multiworld.get_player_name(self.player)
self.connect_name = player_name[:32]
while len(self.connect_name.encode('utf-8')) > 32:
@@ -397,24 +381,21 @@ class SoEWorld(World):
placement_file = ""
out_file = ""
try:
money = self.multiworld.money_modifier[self.player].value
exp = self.multiworld.exp_modifier[self.player].value
money = self.options.money_modifier.value
exp = self.options.exp_modifier.value
switches: typing.List[str] = []
if self.multiworld.death_link[self.player].value:
if self.options.death_link.value:
switches.append("--death-link")
if self.energy_core == EnergyCore.option_fragments:
switches.extend(('--available-fragments', str(self.available_fragments),
'--required-fragments', str(self.required_fragments)))
if self.options.energy_core == EnergyCore.option_fragments:
switches.extend(('--available-fragments', str(self.options.available_fragments.value),
'--required-fragments', str(self.options.required_fragments.value)))
rom_file = get_base_rom_path()
out_base = output_path(output_directory, self.multiworld.get_out_file_name_base(self.player))
out_file = out_base + '.sfc'
placement_file = out_base + '.txt'
patch_file = out_base + '.apsoe'
flags = 'l' # spoiler log
for option_name in self.option_definitions:
option = getattr(self.multiworld, option_name)[self.player]
if hasattr(option, 'to_flag'):
flags += option.to_flag()
flags += self.options.flags
with open(placement_file, "wb") as f: # generate placement file
for location in self.multiworld.get_locations(self.player):
@@ -448,7 +429,7 @@ class SoEWorld(World):
except FileNotFoundError:
pass
def modify_multidata(self, multidata: dict):
def modify_multidata(self, multidata: typing.Dict[str, typing.Any]) -> None:
# wait for self.connect_name to be available.
self.connect_name_available_event.wait()
# we skip in case of error, so that the original error in the output thread is the one that gets raised
@@ -457,7 +438,7 @@ class SoEWorld(World):
multidata["connect_names"][self.connect_name] = payload
def get_filler_item_name(self) -> str:
return self.multiworld.random.choice(list(self.item_name_groups["Ingredients"]))
return self.random.choice(list(self.item_name_groups["Ingredients"]))
class SoEItem(Item):

85
worlds/soe/logic.py Normal file
View File

@@ -0,0 +1,85 @@
import typing
from typing import Callable, Set
from . import pyevermizer
from .options import EnergyCore, OutOfBounds, SequenceBreaks, SoEOptions
if typing.TYPE_CHECKING:
from BaseClasses import CollectionState
# TODO: Options may preset certain progress steps (i.e. P_ROCK_SKIP), set in generate_early?
# TODO: resolve/flatten/expand rules to get rid of recursion below where possible
# Logic.rules are all rules including locations, excluding those with no progress (i.e. locations that only drop items)
rules = [rule for rule in pyevermizer.get_logic() if len(rule.provides) > 0]
# Logic.items are all items and extra items excluding non-progression items and duplicates
item_names: Set[str] = set()
items = [item for item in filter(lambda item: item.progression, pyevermizer.get_items() + pyevermizer.get_extra_items())
if item.name not in item_names and not item_names.add(item.name)] # type: ignore[func-returns-value]
class SoEPlayerLogic:
__slots__ = "player", "out_of_bounds", "sequence_breaks", "has"
player: int
out_of_bounds: bool
sequence_breaks: bool
has: Callable[..., bool]
"""
Returns True if count of one of evermizer's progress steps is reached based on collected items. i.e. 2 * P_DE
"""
def __init__(self, player: int, options: "SoEOptions"):
self.player = player
self.out_of_bounds = options.out_of_bounds == OutOfBounds.option_logic
self.sequence_breaks = options.sequence_breaks == SequenceBreaks.option_logic
if options.energy_core == EnergyCore.option_fragments:
# override logic for energy core fragments
required_fragments = options.required_fragments.value
def fragmented_has(state: "CollectionState", progress: int, count: int = 1) -> bool:
if progress == pyevermizer.P_ENERGY_CORE:
progress = pyevermizer.P_CORE_FRAGMENT
count = required_fragments
return self._has(state, progress, count)
self.has = fragmented_has
else:
# default (energy core) logic
self.has = self._has
def _count(self, state: "CollectionState", progress: int, max_count: int = 0) -> int:
"""
Returns reached count of one of evermizer's progress steps based on collected items.
i.e. returns 0-3 for P_DE based on items providing CHECK_BOSS,DIAMOND_EYE_DROP
"""
n = 0
for item in items:
for pvd in item.provides:
if pvd[1] == progress:
if state.has(item.name, self.player):
n += state.count(item.name, self.player) * pvd[0]
if n >= max_count > 0:
return n
for rule in rules:
for pvd in rule.provides:
if pvd[1] == progress and pvd[0] > 0:
has = True
for req in rule.requires:
if not self.has(state, req[1], req[0]):
has = False
break
if has:
n += pvd[0]
if n >= max_count > 0:
return n
return n
def _has(self, state: "CollectionState", progress: int, count: int = 1) -> bool:
"""Default implementation of has"""
if self.out_of_bounds is True and progress == pyevermizer.P_ALLOW_OOB:
return True
if self.sequence_breaks is True and progress == pyevermizer.P_ALLOW_SEQUENCE_BREAKS:
return True
return self._count(state, progress, count) >= count

View File

@@ -1,16 +1,18 @@
import typing
from dataclasses import dataclass, fields
from typing import Any, cast, Dict, Iterator, List, Tuple, Protocol
from Options import Range, Choice, Toggle, DefaultOnToggle, AssembleOptions, DeathLink, ProgressionBalancing
from Options import AssembleOptions, Choice, DeathLink, DefaultOnToggle, Option, PerGameCommonOptions, \
ProgressionBalancing, Range, Toggle
# typing boilerplate
class FlagsProtocol(typing.Protocol):
class FlagsProtocol(Protocol):
value: int
default: int
flags: typing.List[str]
flags: List[str]
class FlagProtocol(typing.Protocol):
class FlagProtocol(Protocol):
value: int
default: int
flag: str
@@ -18,7 +20,7 @@ class FlagProtocol(typing.Protocol):
# meta options
class EvermizerFlags:
flags: typing.List[str]
flags: List[str]
def to_flag(self: FlagsProtocol) -> str:
return self.flags[self.value]
@@ -200,13 +202,13 @@ class TrapCount(Range):
# more meta options
class ItemChanceMeta(AssembleOptions):
def __new__(mcs, name, bases, attrs):
def __new__(mcs, name: str, bases: Tuple[type], attrs: Dict[Any, Any]) -> "ItemChanceMeta":
if 'item_name' in attrs:
attrs["display_name"] = f"{attrs['item_name']} Chance"
attrs["range_start"] = 0
attrs["range_end"] = 100
return super(ItemChanceMeta, mcs).__new__(mcs, name, bases, attrs)
cls = super(ItemChanceMeta, mcs).__new__(mcs, name, bases, attrs)
return cast(ItemChanceMeta, cls)
class TrapChance(Range, metaclass=ItemChanceMeta):
@@ -247,33 +249,52 @@ class SoEProgressionBalancing(ProgressionBalancing):
special_range_names = {**ProgressionBalancing.special_range_names, "normal": default}
soe_options: typing.Dict[str, AssembleOptions] = {
"difficulty": Difficulty,
"energy_core": EnergyCore,
"required_fragments": RequiredFragments,
"available_fragments": AvailableFragments,
"money_modifier": MoneyModifier,
"exp_modifier": ExpModifier,
"sequence_breaks": SequenceBreaks,
"out_of_bounds": OutOfBounds,
"fix_cheats": FixCheats,
"fix_infinite_ammo": FixInfiniteAmmo,
"fix_atlas_glitch": FixAtlasGlitch,
"fix_wings_glitch": FixWingsGlitch,
"shorter_dialogs": ShorterDialogs,
"short_boss_rush": ShortBossRush,
"ingredienizer": Ingredienizer,
"sniffamizer": Sniffamizer,
"callbeadamizer": Callbeadamizer,
"musicmizer": Musicmizer,
"doggomizer": Doggomizer,
"turdo_mode": TurdoMode,
"death_link": DeathLink,
"trap_count": TrapCount,
"trap_chance_quake": TrapChanceQuake,
"trap_chance_poison": TrapChancePoison,
"trap_chance_confound": TrapChanceConfound,
"trap_chance_hud": TrapChanceHUD,
"trap_chance_ohko": TrapChanceOHKO,
"progression_balancing": SoEProgressionBalancing,
}
# noinspection SpellCheckingInspection
@dataclass
class SoEOptions(PerGameCommonOptions):
difficulty: Difficulty
energy_core: EnergyCore
required_fragments: RequiredFragments
available_fragments: AvailableFragments
money_modifier: MoneyModifier
exp_modifier: ExpModifier
sequence_breaks: SequenceBreaks
out_of_bounds: OutOfBounds
fix_cheats: FixCheats
fix_infinite_ammo: FixInfiniteAmmo
fix_atlas_glitch: FixAtlasGlitch
fix_wings_glitch: FixWingsGlitch
shorter_dialogs: ShorterDialogs
short_boss_rush: ShortBossRush
ingredienizer: Ingredienizer
sniffamizer: Sniffamizer
callbeadamizer: Callbeadamizer
musicmizer: Musicmizer
doggomizer: Doggomizer
turdo_mode: TurdoMode
death_link: DeathLink
trap_count: TrapCount
trap_chance_quake: TrapChanceQuake
trap_chance_poison: TrapChancePoison
trap_chance_confound: TrapChanceConfound
trap_chance_hud: TrapChanceHUD
trap_chance_ohko: TrapChanceOHKO
progression_balancing: SoEProgressionBalancing
@property
def trap_chances(self) -> Iterator[TrapChance]:
for field in fields(self):
option = getattr(self, field.name)
if isinstance(option, TrapChance):
yield option
@property
def flags(self) -> str:
flags = ''
for field in fields(self):
option = getattr(self, field.name)
if isinstance(option, (EvermizerFlag, EvermizerFlags)):
assert isinstance(option, Option)
# noinspection PyUnresolvedReferences
flags += option.to_flag()
return flags

View File

@@ -1,5 +1,5 @@
import os
from typing import Optional
from typing import BinaryIO, Optional
import Utils
from worlds.Files import APDeltaPatch
@@ -30,7 +30,7 @@ def get_base_rom_path(file_name: Optional[str] = None) -> str:
return file_name
def read_rom(stream, strip_header=True) -> bytes:
def read_rom(stream: BinaryIO, strip_header: bool = True) -> bytes:
"""Reads rom into bytearray and optionally strips off any smc header"""
data = stream.read()
if strip_header and len(data) % 0x400 == 0x200:
@@ -40,5 +40,5 @@ def read_rom(stream, strip_header=True) -> bytes:
if __name__ == '__main__':
import sys
print('Please use ../../Patch.py', file=sys.stderr)
print('Please use ../../patch.py', file=sys.stderr)
sys.exit(1)

View File

@@ -1,4 +1,4 @@
from test.TestBase import WorldTestBase
from test.bases import WorldTestBase
from typing import Iterable
@@ -6,7 +6,7 @@ class SoETestBase(WorldTestBase):
game = "Secret of Evermore"
def assertLocationReachability(self, reachable: Iterable[str] = (), unreachable: Iterable[str] = (),
satisfied=True) -> None:
satisfied: bool = True) -> None:
"""
Tests that unreachable can't be reached. Tests that reachable can be reached if satisfied=True.
Usage: test with satisfied=False, collect requirements into state, test again with satisfied=True
@@ -18,3 +18,14 @@ class SoETestBase(WorldTestBase):
for location in unreachable:
self.assertFalse(self.can_reach_location(location),
f"{location} is reachable but shouldn't be")
def testRocketPartsExist(self) -> None:
"""Tests that rocket parts exist and are unique"""
self.assertEqual(len(self.get_items_by_name("Gauge")), 1)
self.assertEqual(len(self.get_items_by_name("Wheel")), 1)
diamond_eyes = self.get_items_by_name("Diamond Eye")
self.assertEqual(len(diamond_eyes), 3)
# verify diamond eyes are individual items
self.assertFalse(diamond_eyes[0] is diamond_eyes[1])
self.assertFalse(diamond_eyes[0] is diamond_eyes[2])
self.assertFalse(diamond_eyes[1] is diamond_eyes[2])

View File

@@ -4,10 +4,10 @@ from . import SoETestBase
class AccessTest(SoETestBase):
@staticmethod
def _resolveGourds(gourds: typing.Dict[str, typing.Iterable[int]]):
def _resolveGourds(gourds: typing.Mapping[str, typing.Iterable[int]]) -> typing.List[str]:
return [f"{name} #{number}" for name, numbers in gourds.items() for number in numbers]
def testBronzeAxe(self):
def test_bronze_axe(self) -> None:
gourds = {
"Pyramid bottom": (118, 121, 122, 123, 124, 125),
"Pyramid top": (140,)
@@ -16,7 +16,7 @@ class AccessTest(SoETestBase):
items = [["Bronze Axe"]]
self.assertAccessDependency(locations, items)
def testBronzeSpearPlus(self):
def test_bronze_spear_plus(self) -> None:
locations = ["Megataur"]
items = [["Bronze Spear"], ["Lance (Weapon)"], ["Laser Lance"]]
self.assertAccessDependency(locations, items)

View File

@@ -8,7 +8,7 @@ class TestFragmentGoal(SoETestBase):
"required_fragments": 20,
}
def testFragments(self):
def test_fragments(self) -> None:
self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Gauge"])
self.assertBeatable(False) # 0 fragments
fragments = self.get_items_by_name("Energy Core Fragment")
@@ -24,11 +24,11 @@ class TestFragmentGoal(SoETestBase):
self.assertEqual(self.count("Energy Core Fragment"), 21)
self.assertBeatable(True)
def testNoWeapon(self):
def test_no_weapon(self) -> None:
self.collect_by_name(["Diamond Eye", "Wheel", "Gauge", "Energy Core Fragment"])
self.assertBeatable(False)
def testNoRocket(self):
def test_no_rocket(self) -> None:
self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Energy Core Fragment"])
self.assertBeatable(False)
@@ -38,16 +38,16 @@ class TestShuffleGoal(SoETestBase):
"energy_core": "shuffle",
}
def testCore(self):
def test_core(self) -> None:
self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Gauge"])
self.assertBeatable(False)
self.collect_by_name(["Energy Core"])
self.assertBeatable(True)
def testNoWeapon(self):
def test_no_weapon(self) -> None:
self.collect_by_name(["Diamond Eye", "Wheel", "Gauge", "Energy Core"])
self.assertBeatable(False)
def testNoRocket(self):
def test_no_rocket(self) -> None:
self.collect_by_name(["Gladiator Sword", "Diamond Eye", "Wheel", "Energy Core"])
self.assertBeatable(False)

View File

@@ -6,7 +6,7 @@ class OoBTest(SoETestBase):
"""Tests that 'on' doesn't put out-of-bounds in logic. This is also the test base for OoB in logic."""
options: typing.Dict[str, typing.Any] = {"out_of_bounds": "on"}
def testOoBAccess(self):
def test_oob_access(self) -> None:
in_logic = self.options["out_of_bounds"] == "logic"
# some locations that just need a weapon + OoB
@@ -37,7 +37,7 @@ class OoBTest(SoETestBase):
self.collect_by_name("Diamond Eye")
self.assertLocationReachability(reachable=de_reachable, unreachable=de_unreachable, satisfied=in_logic)
def testOoBGoal(self):
def test_oob_goal(self) -> None:
# still need Energy Core with OoB if sequence breaks are not in logic
for item in ["Gladiator Sword", "Diamond Eye", "Wheel", "Gauge"]:
self.collect_by_name(item)

View File

@@ -6,7 +6,7 @@ class SequenceBreaksTest(SoETestBase):
"""Tests that 'on' doesn't put sequence breaks in logic. This is also the test base for in-logic."""
options: typing.Dict[str, typing.Any] = {"sequence_breaks": "on"}
def testSequenceBreaksAccess(self):
def test_sequence_breaks_access(self) -> None:
in_logic = self.options["sequence_breaks"] == "logic"
# some locations that just need any weapon + sequence break
@@ -30,7 +30,7 @@ class SequenceBreaksTest(SoETestBase):
self.collect_by_name("Bronze Spear") # Escape now just needs either Megataur or Rimsala dead
self.assertEqual(self.can_reach_location("Escape"), in_logic)
def testSequenceBreaksGoal(self):
def test_sequence_breaks_goal(self) -> None:
in_logic = self.options["sequence_breaks"] == "logic"
# don't need Energy Core with sequence breaks in logic

View File

@@ -0,0 +1,56 @@
import typing
from dataclasses import fields
from . import SoETestBase
from ..options import SoEOptions
if typing.TYPE_CHECKING:
from .. import SoEWorld
class Bases:
# class in class to avoid running tests for TrapTest class
class TrapTestBase(SoETestBase):
"""Test base for trap tests"""
option_name_to_item_name = {
# filtering by name here validates that there is no confusion between name and type
field.name: field.type.item_name for field in fields(SoEOptions) if field.name.startswith("trap_chance_")
}
def test_dataclass(self) -> None:
"""Test that the dataclass helper property returns the expected sequence"""
self.assertGreater(len(self.option_name_to_item_name), 0, "Expected more than 0 trap types")
world: "SoEWorld" = typing.cast("SoEWorld", self.multiworld.worlds[1])
item_name_to_rolled_option = {option.item_name: option for option in world.options.trap_chances}
# compare that all fields are present - that is property in dataclass and selector code in test line up
self.assertEqual(sorted(self.option_name_to_item_name.values()), sorted(item_name_to_rolled_option),
"field names probably do not match field types")
# sanity check that chances are correctly set and returned by property
for option_name, item_name in self.option_name_to_item_name.items():
self.assertEqual(item_name_to_rolled_option[item_name].value,
self.options.get(option_name, item_name_to_rolled_option[item_name].default))
def test_trap_count(self) -> None:
"""Test that total trap count is correct"""
self.assertEqual(self.options["trap_count"],
len(self.get_items_by_name(self.option_name_to_item_name.values())))
class TestTrapAllZeroChance(Bases.TrapTestBase):
"""Tests all zero chances still gives traps if trap_count is set."""
options: typing.Dict[str, typing.Any] = {
"trap_count": 1,
**{name: 0 for name in Bases.TrapTestBase.option_name_to_item_name}
}
class TestTrapNoConfound(Bases.TrapTestBase):
"""Tests that one zero chance does not give that trap."""
options: typing.Dict[str, typing.Any] = {
"trap_count": 99,
"trap_chance_confound": 0,
}
def test_no_confound_trap(self) -> None:
self.assertEqual(self.option_name_to_item_name["trap_chance_confound"], "Confound Trap")
self.assertEqual(len(self.get_items_by_name("Confound Trap")), 0)

View File

@@ -170,6 +170,8 @@ def set_entrance_rules(logic, multiworld, player, world_options: StardewValleyOp
logic.received("Bus Repair").simplify())
MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.enter_skull_cavern, player),
logic.received(Wallet.skull_key).simplify())
MultiWorldRules.set_rule(multiworld.get_entrance(Entrance.enter_casino, player),
logic.received("Club Card").simplify())
for floor in range(25, 200 + 25, 25):
MultiWorldRules.set_rule(multiworld.get_entrance(dig_to_skull_floor(floor), player),
logic.can_mine_to_skull_cavern_floor(floor).simplify())

View File

@@ -94,17 +94,17 @@ def get_pool_core(world):
# Starting Weapon
start_weapon_locations = starting_weapon_locations.copy()
final_starting_weapons = [weapon for weapon in starting_weapons
if weapon not in world.multiworld.non_local_items[world.player]]
if weapon not in world.options.non_local_items]
if not final_starting_weapons:
final_starting_weapons = starting_weapons
starting_weapon = random.choice(final_starting_weapons)
if world.multiworld.StartingPosition[world.player] == StartingPosition.option_safe:
if world.options.StartingPosition == StartingPosition.option_safe:
placed_items[start_weapon_locations[0]] = starting_weapon
elif world.multiworld.StartingPosition[world.player] in \
elif world.options.StartingPosition in \
[StartingPosition.option_unsafe, StartingPosition.option_dangerous]:
if world.multiworld.StartingPosition[world.player] == StartingPosition.option_dangerous:
if world.options.StartingPosition == StartingPosition.option_dangerous:
for location in dangerous_weapon_locations:
if world.multiworld.ExpandedPool[world.player] or "Drop" not in location:
if world.options.ExpandedPool or "Drop" not in location:
start_weapon_locations.append(location)
placed_items[random.choice(start_weapon_locations)] = starting_weapon
else:
@@ -115,7 +115,7 @@ def get_pool_core(world):
# Triforce Fragments
fragment = "Triforce Fragment"
if world.multiworld.ExpandedPool[world.player]:
if world.options.ExpandedPool:
possible_level_locations = [location for location in all_level_locations
if location not in level_locations[8]]
else:
@@ -125,15 +125,15 @@ def get_pool_core(world):
if location in possible_level_locations:
possible_level_locations.remove(location)
for level in range(1, 9):
if world.multiworld.TriforceLocations[world.player] == TriforceLocations.option_vanilla:
if world.options.TriforceLocations == TriforceLocations.option_vanilla:
placed_items[f"Level {level} Triforce"] = fragment
elif world.multiworld.TriforceLocations[world.player] == TriforceLocations.option_dungeons:
elif world.options.TriforceLocations == TriforceLocations.option_dungeons:
placed_items[possible_level_locations.pop(random.randint(0, len(possible_level_locations) - 1))] = fragment
else:
pool.append(fragment)
# Level 9 junk fill
if world.multiworld.ExpandedPool[world.player] > 0:
if world.options.ExpandedPool > 0:
spots = random.sample(level_locations[8], len(level_locations[8]) // 2)
for spot in spots:
junk = random.choice(list(minor_items.keys()))
@@ -142,7 +142,7 @@ def get_pool_core(world):
# Finish Pool
final_pool = basic_pool
if world.multiworld.ExpandedPool[world.player]:
if world.options.ExpandedPool:
final_pool = {
item: basic_pool.get(item, 0) + minor_items.get(item, 0) + take_any_items.get(item, 0)
for item in set(basic_pool) | set(minor_items) | set(take_any_items)

View File

@@ -1,5 +1,6 @@
import typing
from Options import Option, DefaultOnToggle, Choice
from dataclasses import dataclass
from Options import Option, DefaultOnToggle, Choice, PerGameCommonOptions
class ExpandedPool(DefaultOnToggle):
@@ -32,9 +33,8 @@ class StartingPosition(Choice):
option_dangerous = 2
option_very_dangerous = 3
tloz_options: typing.Dict[str, type(Option)] = {
"ExpandedPool": ExpandedPool,
"TriforceLocations": TriforceLocations,
"StartingPosition": StartingPosition
}
@dataclass
class TlozOptions(PerGameCommonOptions):
ExpandedPool: ExpandedPool
TriforceLocations: TriforceLocations
StartingPosition: StartingPosition

View File

@@ -11,6 +11,7 @@ if TYPE_CHECKING:
def set_rules(tloz_world: "TLoZWorld"):
player = tloz_world.player
world = tloz_world.multiworld
options = tloz_world.options
# Boss events for a nicer spoiler log play through
for level in range(1, 9):
@@ -23,7 +24,7 @@ def set_rules(tloz_world: "TLoZWorld"):
# No dungeons without weapons except for the dangerous weapon locations if we're dangerous, no unsafe dungeons
for i, level in enumerate(tloz_world.levels[1:10]):
for location in level.locations:
if world.StartingPosition[player] < StartingPosition.option_dangerous \
if options.StartingPosition < StartingPosition.option_dangerous \
or location.name not in dangerous_weapon_locations:
add_rule(world.get_location(location.name, player),
lambda state: state.has_group("weapons", player))
@@ -66,7 +67,7 @@ def set_rules(tloz_world: "TLoZWorld"):
lambda state: state.has("Recorder", player))
add_rule(world.get_location("Level 7 Boss", player),
lambda state: state.has("Recorder", player))
if world.ExpandedPool[player]:
if options.ExpandedPool:
add_rule(world.get_location("Level 7 Key Drop (Stalfos)", player),
lambda state: state.has("Recorder", player))
add_rule(world.get_location("Level 7 Bomb Drop (Digdogger)", player),
@@ -75,13 +76,13 @@ def set_rules(tloz_world: "TLoZWorld"):
lambda state: state.has("Recorder", player))
for location in food_locations:
if world.ExpandedPool[player] or "Drop" not in location:
if options.ExpandedPool or "Drop" not in location:
add_rule(world.get_location(location, player),
lambda state: state.has("Food", player))
add_rule(world.get_location("Level 8 Item (Magical Key)", player),
lambda state: state.has("Bow", player) and state.has_group("arrows", player))
if world.ExpandedPool[player]:
if options.ExpandedPool:
add_rule(world.get_location("Level 8 Bomb Drop (Darknuts North)", player),
lambda state: state.has("Bow", player) and state.has_group("arrows", player))
@@ -106,13 +107,13 @@ def set_rules(tloz_world: "TLoZWorld"):
for location in stepladder_locations:
add_rule(world.get_location(location, player),
lambda state: state.has("Stepladder", player))
if world.ExpandedPool[player]:
if options.ExpandedPool:
for location in stepladder_locations_expanded:
add_rule(world.get_location(location, player),
lambda state: state.has("Stepladder", player))
# Don't allow Take Any Items until we can actually get in one
if world.ExpandedPool[player]:
if options.ExpandedPool:
add_rule(world.get_location("Take Any Item Left", player),
lambda state: state.has_group("candles", player) or
state.has("Raft", player))

View File

@@ -13,7 +13,7 @@ from .ItemPool import generate_itempool, starting_weapons, dangerous_weapon_loca
from .Items import item_table, item_prices, item_game_ids
from .Locations import location_table, level_locations, major_locations, shop_locations, all_level_locations, \
standard_level_locations, shop_price_location_ids, secret_money_ids, location_ids, food_locations
from .Options import tloz_options
from .Options import TlozOptions
from .Rom import TLoZDeltaPatch, get_base_rom_path, first_quest_dungeon_items_early, first_quest_dungeon_items_late
from .Rules import set_rules
from worlds.AutoWorld import World, WebWorld
@@ -63,7 +63,8 @@ class TLoZWorld(World):
This randomizer shuffles all the items in the game around, leading to a new adventure
every time.
"""
option_definitions = tloz_options
options_dataclass = TlozOptions
options: TlozOptions
settings: typing.ClassVar[TLoZSettings]
game = "The Legend of Zelda"
topology_present = False
@@ -132,7 +133,7 @@ class TLoZWorld(World):
for i, level in enumerate(level_locations):
for location in level:
if self.multiworld.ExpandedPool[self.player] or "Drop" not in location:
if self.options.ExpandedPool or "Drop" not in location:
self.levels[i + 1].locations.append(
self.create_location(location, self.location_name_to_id[location], self.levels[i + 1]))
@@ -144,7 +145,7 @@ class TLoZWorld(World):
self.levels[level].locations.append(boss_event)
for location in major_locations:
if self.multiworld.ExpandedPool[self.player] or "Take Any" not in location:
if self.options.ExpandedPool or "Take Any" not in location:
overworld.locations.append(
self.create_location(location, self.location_name_to_id[location], overworld))
@@ -311,7 +312,7 @@ class TLoZWorld(World):
return self.multiworld.random.choice(self.filler_items)
def fill_slot_data(self) -> Dict[str, Any]:
if self.multiworld.ExpandedPool[self.player]:
if self.options.ExpandedPool:
take_any_left = self.multiworld.get_location("Take Any Item Left", self.player).item
take_any_middle = self.multiworld.get_location("Take Any Item Middle", self.player).item
take_any_right = self.multiworld.get_location("Take Any Item Right", self.player).item

279
worlds/tunic/__init__.py Normal file
View File

@@ -0,0 +1,279 @@
from typing import Dict, List, Any
from BaseClasses import Region, Location, Item, Tutorial, ItemClassification
from .items import item_name_to_id, item_table, item_name_groups, fool_tiers, filler_items, slot_data_item_names
from .locations import location_table, location_name_groups, location_name_to_id, hexagon_locations
from .rules import set_location_rules, set_region_rules, randomize_ability_unlocks, gold_hexagon
from .er_rules import set_er_location_rules
from .regions import tunic_regions
from .er_scripts import create_er_regions
from .options import TunicOptions
from worlds.AutoWorld import WebWorld, World
from decimal import Decimal, ROUND_HALF_UP
class TunicWeb(WebWorld):
tutorials = [
Tutorial(
tutorial_name="Multiworld Setup Guide",
description="A guide to setting up the TUNIC Randomizer for Archipelago multiworld games.",
language="English",
file_name="setup_en.md",
link="setup/en",
authors=["SilentDestroyer"]
)
]
theme = "grassFlowers"
game = "Tunic"
class TunicItem(Item):
game: str = "Tunic"
class TunicLocation(Location):
game: str = "Tunic"
class TunicWorld(World):
"""
Explore a land filled with lost legends, ancient powers, and ferocious monsters in TUNIC, an isometric action game
about a small fox on a big adventure. Stranded on a mysterious beach, armed with only your own curiosity, you will
confront colossal beasts, collect strange and powerful items, and unravel long-lost secrets. Be brave, tiny fox!
"""
game = "Tunic"
web = TunicWeb()
data_version = 2
options: TunicOptions
options_dataclass = TunicOptions
item_name_groups = item_name_groups
location_name_groups = location_name_groups
item_name_to_id = item_name_to_id
location_name_to_id = location_name_to_id
ability_unlocks: Dict[str, int]
slot_data_items: List[TunicItem]
tunic_portal_pairs: Dict[str, str]
er_portal_hints: Dict[int, str]
def generate_early(self) -> None:
if self.options.start_with_sword and "Sword" not in self.options.start_inventory:
self.options.start_inventory.value["Sword"] = 1
def create_item(self, name: str) -> TunicItem:
item_data = item_table[name]
return TunicItem(name, item_data.classification, self.item_name_to_id[name], self.player)
def create_items(self) -> None:
keys_behind_bosses = self.options.keys_behind_bosses
hexagon_quest = self.options.hexagon_quest
sword_progression = self.options.sword_progression
tunic_items: List[TunicItem] = []
self.slot_data_items = []
items_to_create: Dict[str, int] = {item: data.quantity_in_item_pool for item, data in item_table.items()}
for money_fool in fool_tiers[self.options.fool_traps]:
items_to_create["Fool Trap"] += items_to_create[money_fool]
items_to_create[money_fool] = 0
if sword_progression:
items_to_create["Stick"] = 0
items_to_create["Sword"] = 0
else:
items_to_create["Sword Upgrade"] = 0
if self.options.laurels_location:
laurels = self.create_item("Hero's Laurels")
if self.options.laurels_location == "6_coins":
self.multiworld.get_location("Coins in the Well - 6 Coins", self.player).place_locked_item(laurels)
elif self.options.laurels_location == "10_coins":
self.multiworld.get_location("Coins in the Well - 10 Coins", self.player).place_locked_item(laurels)
elif self.options.laurels_location == "10_fairies":
self.multiworld.get_location("Secret Gathering Place - 10 Fairy Reward", self.player).place_locked_item(laurels)
self.slot_data_items.append(laurels)
items_to_create["Hero's Laurels"] = 0
if keys_behind_bosses:
for rgb_hexagon, location in hexagon_locations.items():
hex_item = self.create_item(gold_hexagon if hexagon_quest else rgb_hexagon)
self.multiworld.get_location(location, self.player).place_locked_item(hex_item)
self.slot_data_items.append(hex_item)
items_to_create[rgb_hexagon] = 0
items_to_create[gold_hexagon] -= 3
if hexagon_quest:
# Calculate number of hexagons in item pool
hexagon_goal = self.options.hexagon_goal
extra_hexagons = self.options.extra_hexagon_percentage
items_to_create[gold_hexagon] += int((Decimal(100 + extra_hexagons) / 100 * hexagon_goal).to_integral_value(rounding=ROUND_HALF_UP))
# Replace pages and normal hexagons with filler
for replaced_item in list(filter(lambda item: "Pages" in item or item in hexagon_locations, items_to_create)):
items_to_create[self.get_filler_item_name()] += items_to_create[replaced_item]
items_to_create[replaced_item] = 0
# Filler items that are still in the item pool to swap out
available_filler: List[str] = [filler for filler in items_to_create if items_to_create[filler] > 0 and
item_table[filler].classification == ItemClassification.filler]
# Remove filler to make room for extra hexagons
for i in range(0, items_to_create[gold_hexagon]):
fill = self.random.choice(available_filler)
items_to_create[fill] -= 1
if items_to_create[fill] == 0:
available_filler.remove(fill)
if self.options.maskless:
mask_item = TunicItem("Scavenger Mask", ItemClassification.useful, self.item_name_to_id["Scavenger Mask"], self.player)
tunic_items.append(mask_item)
items_to_create["Scavenger Mask"] = 0
if self.options.lanternless:
mask_item = TunicItem("Lantern", ItemClassification.useful, self.item_name_to_id["Lantern"], self.player)
tunic_items.append(mask_item)
items_to_create["Lantern"] = 0
for item, quantity in items_to_create.items():
for i in range(0, quantity):
tunic_item: TunicItem = self.create_item(item)
if item in slot_data_item_names:
self.slot_data_items.append(tunic_item)
tunic_items.append(tunic_item)
self.multiworld.itempool += tunic_items
def create_regions(self) -> None:
self.tunic_portal_pairs = {}
self.er_portal_hints = {}
self.ability_unlocks = randomize_ability_unlocks(self.random, self.options)
if self.options.entrance_rando:
portal_pairs, portal_hints = create_er_regions(self)
for portal1, portal2 in portal_pairs.items():
self.tunic_portal_pairs[portal1.scene_destination()] = portal2.scene_destination()
self.er_portal_hints = portal_hints
else:
for region_name in tunic_regions:
region = Region(region_name, self.player, self.multiworld)
self.multiworld.regions.append(region)
for region_name, exits in tunic_regions.items():
region = self.multiworld.get_region(region_name, self.player)
region.add_exits(exits)
for location_name, location_id in self.location_name_to_id.items():
region = self.multiworld.get_region(location_table[location_name].region, self.player)
location = TunicLocation(self.player, location_name, location_id, region)
region.locations.append(location)
victory_region = self.multiworld.get_region("Spirit Arena", self.player)
victory_location = TunicLocation(self.player, "The Heir", None, victory_region)
victory_location.place_locked_item(TunicItem("Victory", ItemClassification.progression, None, self.player))
self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
victory_region.locations.append(victory_location)
def set_rules(self) -> None:
if self.options.entrance_rando:
set_er_location_rules(self, self.ability_unlocks)
else:
set_region_rules(self, self.ability_unlocks)
set_location_rules(self, self.ability_unlocks)
def get_filler_item_name(self) -> str:
return self.random.choice(filler_items)
def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]):
if self.options.entrance_rando:
hint_data[self.player] = self.er_portal_hints
def fill_slot_data(self) -> Dict[str, Any]:
slot_data: Dict[str, Any] = {
"seed": self.random.randint(0, 2147483647),
"start_with_sword": self.options.start_with_sword.value,
"keys_behind_bosses": self.options.keys_behind_bosses.value,
"sword_progression": self.options.sword_progression.value,
"ability_shuffling": self.options.ability_shuffling.value,
"hexagon_quest": self.options.hexagon_quest.value,
"fool_traps": self.options.fool_traps.value,
"entrance_rando": self.options.entrance_rando.value,
"Hexagon Quest Prayer": self.ability_unlocks["Pages 24-25 (Prayer)"],
"Hexagon Quest Holy Cross": self.ability_unlocks["Pages 42-43 (Holy Cross)"],
"Hexagon Quest Ice Rod": self.ability_unlocks["Pages 52-53 (Ice Rod)"],
"Hexagon Quest Goal": self.options.hexagon_goal.value,
"Entrance Rando": self.tunic_portal_pairs
}
for tunic_item in filter(lambda item: item.location is not None and item.code is not None, self.slot_data_items):
if tunic_item.name not in slot_data:
slot_data[tunic_item.name] = []
if tunic_item.name == gold_hexagon and len(slot_data[gold_hexagon]) >= 6:
continue
slot_data[tunic_item.name].extend([tunic_item.location.name, tunic_item.location.player])
for start_item in self.options.start_inventory_from_pool:
if start_item in slot_data_item_names:
if start_item not in slot_data:
slot_data[start_item] = []
for i in range(0, self.options.start_inventory_from_pool[start_item]):
slot_data[start_item].extend(["Your Pocket", self.player])
for plando_item in self.multiworld.plando_items[self.player]:
if plando_item["from_pool"]:
items_to_find = set()
for item_type in [key for key in ["item", "items"] if key in plando_item]:
for item in plando_item[item_type]:
items_to_find.add(item)
for item in items_to_find:
if item in slot_data_item_names:
slot_data[item] = []
for item_location in self.multiworld.find_item_locations(item, self.player):
slot_data[item].extend([item_location.name, item_location.player])
return slot_data
# for the universal tracker, doesn't get called in standard gen
def interpret_slot_data(self, slot_data: Dict[str, Any]) -> None:
# bypassing random yaml settings
self.options.start_with_sword.value = slot_data["start_with_sword"]
self.options.keys_behind_bosses.value = slot_data["keys_behind_bosses"]
self.options.sword_progression.value = slot_data["sword_progression"]
self.options.ability_shuffling.value = slot_data["ability_shuffling"]
self.options.hexagon_quest.value = slot_data["hexagon_quest"]
self.ability_unlocks["Pages 24-25 (Prayer)"] = slot_data["Hexagon Quest Prayer"]
self.ability_unlocks["Pages 42-43 (Holy Cross)"] = slot_data["Hexagon Quest Holy Cross"]
self.ability_unlocks["Pages 52-53 (Ice Rod)"] = slot_data["Hexagon Quest Ice Rod"]
# swapping entrances around so the mapping matches what was generated
if slot_data["entrance_rando"]:
from BaseClasses import Entrance
from .er_data import portal_mapping
entrance_dict: Dict[str, Entrance] = {entrance.name: entrance
for region in self.multiworld.get_regions(self.player)
for entrance in region.entrances}
slot_portals: Dict[str, str] = slot_data["Entrance Rando"]
for portal1, portal2 in slot_portals.items():
portal_name1: str = ""
portal_name2: str = ""
entrance1 = None
entrance2 = None
for portal in portal_mapping:
if portal.scene_destination() == portal1:
portal_name1 = portal.name
if portal.scene_destination() == portal2:
portal_name2 = portal.name
for entrance_name, entrance in entrance_dict.items():
if entrance_name.startswith(portal_name1):
entrance1 = entrance
if entrance_name.startswith(portal_name2):
entrance2 = entrance
if entrance1 is None:
raise Exception("entrance1 not found, portal1 is " + portal1)
if entrance2 is None:
raise Exception("entrance2 not found, portal2 is " + portal2)
entrance1.connected_region = entrance2.parent_region
entrance2.connected_region = entrance1.parent_region

View File

@@ -0,0 +1,64 @@
# TUNIC
## Where is the options page?
The [player options page for this game](../player-options) contains all the options you need to configure and export a config file.
## I haven't played TUNIC before.
**Play vanilla first.** It is **_heavily discouraged_** to play this randomizer before playing the vanilla game.
It is recommended that you achieve both endings in the vanilla game before playing the randomizer.
## What does randomization do to this game?
In the TUNIC Randomizer, every item in the game is randomized. All chests, key item pickups, instruction manual pages, hero relics,
and other unique items are shuffled.<br>
Ability shuffling is an option available from the options page to shuffle certain abilities (prayer, holy cross, and the ice rod combo),
preventing them from being used until they are unlocked.<br>
Enemy randomization and other options are also available and can be turned on in the client mod.
## What is the goal of TUNIC when randomized?
The standard goal is the same as the vanilla game, which is to find the three hexagon keys, at which point you may either Take Your
Rightful Place or seek another path and Share Your Wisdom.
Alternatively, Hexagon Quest is a mode that shuffles a certain number of Gold Questagons into the item pool, with the goal
being to find the required amount of them and then Share Your Wisdom.
## What items from TUNIC can appear in another player's world?
Every item has a chance to appear in another player's world.
## How many checks are in TUNIC?
There are 302 checks located across the world of TUNIC.
## What do items from other worlds look like in TUNIC?
Items belonging to other TUNIC players will either appear as that item directly (if in a freestanding location) or in a
chest with the original chest texture for that item.
Items belonging to non-TUNIC players will either appear as a question-mark block (if in a freestanding location) or in a chest with
a question mark symbol on it. Additionally, non-TUNIC items are color-coded by classification, with green for filler, blue for useful, and gold for progression.
## Is there a tracker pack?
There is a [tracker pack](https://github.com/SapphireSapphic/TunicTracker/releases/latest). It is compatible with both Poptracker and Emotracker. Using Poptracker, it will automatically track checked locations and important items received. It can also automatically tab between maps as you traverse the world. This tracker was originally created by SapphireSapphic and ScoutJD, and has been extensively updated by Br00ty.
There is also a [standalone item tracker](https://github.com/radicoon/tunic-rando-tracker/releases/latest), which tracks what items you have received. It is great for adding an item overlay to streaming setups. This item tracker was created by Radicoon.
## What should I know regarding logic?
- Nighttime is not considered in logic. Every check in the game is obtainable during the day.
- The Cathedral is accessible during the day by using the Hero's Laurels to reach the Overworld fuse near the Swamp entrance.
- The Secret Legend chest at the Cathedral can be obtained during the day by opening the Holy Cross door from the outside.
For Entrance Rando specifically:
- Activating a fuse to turn on a yellow teleporter pad also activates its counterpart in the Far Shore.
- The West Garden fuse can be activated from below.
- You can pray at the tree at the exterior of the Library.
- The elevators in the Rooted Ziggurat only go down.
- The portal in the trophy room of the Old House is active from the start.
- The elevator in Cathedral is immediately usable without activating the fuse. Activating the fuse does nothing.
## What item groups are there?
Bombs, consumables (non-bomb ones), weapons, melee weapons (stick and sword), keys, hexagons, offerings, hero relics, cards, golden treasures, money, pages, and abilities (the three ability pages). There are also a few groups being used for singular items: laurels, orb, dagger, magic rod, holy cross, prayer, ice rod, and progressive sword.
## What location groups are there?
Holy cross (for all holy cross checks), fairies (for the two fairy checks), well (for the coin well checks), and shop. Additionally, for checks that do not fall into the above categories, the name of the region is the name of the location group.

View File

@@ -0,0 +1,65 @@
# TUNIC Setup Guide
## Installation
### Required Software
- [TUNIC](https://tunicgame.com/) for PC (Steam Deck also supported)
- [BepInEx](https://builds.bepinex.dev/projects/bepinex_be/572/BepInEx_UnityIL2CPP_x64_9c2b17f_6.0.0-be.572.zip)
- [TUNIC Randomizer Archipelago Mod](https://github.com/silent-destroyer/tunic-randomizer-archipelago/releases/latest)
### Optional Software
- [TUNIC Randomizer Map Tracker](https://github.com/SapphireSapphic/TunicTracker/releases/latest) (For use with EmoTracker/PopTracker)
- [TUNIC Randomizer Item Auto-tracker](https://github.com/radicoon/tunic-rando-tracker/releases/latest)
### Find Your Relevant Game Directories
Find your TUNIC game installation directory:
- **Steam**: Right click TUNIC in your Steam Library, then *Manage → Browse local files*.<br>
- **Steam Deck**: Hold down the power button, tap "Switch to Desktop", then launch Steam from Desktop Mode to access the above option.
- **PC Game Pass**: In the Xbox PC app, go to the TUNIC game page from your library, click the [...] button next to "Play", then
*Manage → Files → Browse...*<br>
- **Other platforms**: Follow a similar pattern of steps as above to locate your specific game directory.
### Install BepInEx
BepInEx is a general purpose framework for modding Unity games, and is used by the TUNIC Randomizer.
Download [BepInEx](https://builds.bepinex.dev/projects/bepinex_be/572/BepInEx_UnityIL2CPP_x64_9c2b17f_6.0.0-be.572.zip).
If playing on Steam Deck, follow this [guide to set up BepInEx via Proton](https://docs.bepinex.dev/articles/advanced/proton_wine.html).
Extract the contents of the BepInEx .zip file into your TUNIC game directory:<br>
- **Steam**: Steam\steamapps\common\TUNIC<br>
- **PC Game Pass**: XboxGames\Tunic\Content<br>
- **Other platforms**: Place into the same folder that the Tunic_Data/Secret Legend_Data folder is found.
Launch the game once and close it to finish the BepInEx installation.
### Install The TUNIC Randomizer Archipelago Client Mod
Download the latest release of the [TUNIC Randomizer Archipelago Mod](https://github.com/silent-destroyer/tunic-randomizer-archipelago/releases/latest).
The downloaded .zip will contain a folder called `Tunic Archipelago`.
Copy the `Tunic Archipelago` folder into `BepInEx/plugins` in your TUNIC game installation directory.
The filepath to the mod should look like `BepInEx/plugins/Tunic Archipelago/TunicArchipelago.dll`<br>
Launch the game, and if everything was installed correctly you should see `Randomizer + Archipelago Mod Ver. x.y.z` in the top left corner of the title screen!
## Configure Archipelago Options
### Configure Your YAML File
Visit the [TUNIC options page](/games/Tunic/player-options) to generate a YAML with your selected options.
### Configure Your Mod Settings
Launch the game and click the button labeled `Open AP Config` on the Title Screen.
In the menu that opens, fill in *Player*, *Hostname*, *Port*, and *Password* (if required) with the correct information for your room.
Once you've input your information, click on Close. If everything was configured properly, you should see `Status: Connected!` and your chosen game options will be shown under `World Settings`.
An error message will display if the game fails to connect to the server.
Be sure to also look at the in-game options menu for a variety of additional settings, such as enemy randomization!

1001
worlds/tunic/er_data.py Normal file

File diff suppressed because it is too large Load Diff

984
worlds/tunic/er_rules.py Normal file
View File

@@ -0,0 +1,984 @@
from typing import Dict, TYPE_CHECKING
from worlds.generic.Rules import set_rule, forbid_item
from .rules import has_ability, has_sword, has_stick, has_ice_grapple_logic, has_lantern, has_mask, can_ladder_storage
from .er_data import Portal
from BaseClasses import Region
if TYPE_CHECKING:
from . import TunicWorld
laurels = "Hero's Laurels"
grapple = "Magic Orb"
ice_dagger = "Magic Dagger"
fire_wand = "Magic Wand"
lantern = "Lantern"
fairies = "Fairy"
coins = "Golden Coin"
prayer = "Pages 24-25 (Prayer)"
holy_cross = "Pages 42-43 (Holy Cross)"
ice_rod = "Pages 52-53 (Ice Rod)"
key = "Key"
house_key = "Old House Key"
vault_key = "Fortress Vault Key"
mask = "Scavenger Mask"
red_hexagon = "Red Questagon"
green_hexagon = "Green Questagon"
blue_hexagon = "Blue Questagon"
gold_hexagon = "Gold Questagon"
def set_er_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int], regions: Dict[str, Region],
portal_pairs: Dict[Portal, Portal]) -> None:
player = world.player
options = world.options
regions["Menu"].connect(
connecting_region=regions["Overworld"])
# Overworld
regions["Overworld"].connect(
connecting_region=regions["Overworld Holy Cross"],
rule=lambda state: has_ability(state, player, holy_cross, options, ability_unlocks))
regions["Overworld"].connect(
connecting_region=regions["Overworld Belltower"],
rule=lambda state: state.has(laurels, player))
regions["Overworld Belltower"].connect(
connecting_region=regions["Overworld"])
# nmg: can laurels through the ruined passage door
regions["Overworld"].connect(
connecting_region=regions["Overworld Ruined Passage Door"],
rule=lambda state: state.has(key, player, 2)
or (state.has(laurels, player) and options.logic_rules))
regions["Overworld"].connect(
connecting_region=regions["Overworld Laurels"],
rule=lambda state: state.has(laurels, player))
regions["Overworld Laurels"].connect(
connecting_region=regions["Overworld"],
rule=lambda state: state.has(laurels, player))
# nmg: can ice grapple through the door
regions["Overworld"].connect(
connecting_region=regions["Overworld Old House Door"],
rule=lambda state: state.has(house_key, player)
or has_ice_grapple_logic(False, state, player, options, ability_unlocks))
# not including ice grapple through this because it's very tedious to get an enemy here
regions["Overworld"].connect(
connecting_region=regions["Overworld Southeast Cross Door"],
rule=lambda state: has_ability(state, player, holy_cross, options, ability_unlocks))
regions["Overworld Southeast Cross Door"].connect(
connecting_region=regions["Overworld"],
rule=lambda state: has_ability(state, player, holy_cross, options, ability_unlocks))
# not including ice grapple through this because we're not including it on the other door
regions["Overworld"].connect(
connecting_region=regions["Overworld Fountain Cross Door"],
rule=lambda state: has_ability(state, player, holy_cross, options, ability_unlocks))
regions["Overworld Fountain Cross Door"].connect(
connecting_region=regions["Overworld"])
regions["Overworld"].connect(
connecting_region=regions["Overworld Town Portal"],
rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks))
regions["Overworld Town Portal"].connect(
connecting_region=regions["Overworld"])
regions["Overworld"].connect(
connecting_region=regions["Overworld Spawn Portal"],
rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks))
regions["Overworld Spawn Portal"].connect(
connecting_region=regions["Overworld"])
# nmg: ice grapple through temple door
regions["Overworld"].connect(
connecting_region=regions["Overworld Temple Door"],
name="Overworld Temple Door",
rule=lambda state: state.has_all({"Ring Eastern Bell", "Ring Western Bell"}, player)
or has_ice_grapple_logic(False, state, player, options, ability_unlocks))
# Overworld side areas
regions["Old House Front"].connect(
connecting_region=regions["Old House Back"])
# nmg: laurels through the gate
regions["Old House Back"].connect(
connecting_region=regions["Old House Front"],
rule=lambda state: state.has(laurels, player) and options.logic_rules)
regions["Sealed Temple"].connect(
connecting_region=regions["Sealed Temple Rafters"])
regions["Sealed Temple Rafters"].connect(
connecting_region=regions["Sealed Temple"],
rule=lambda state: state.has(laurels, player))
regions["Furnace Walking Path"].connect(
connecting_region=regions["Furnace Ladder Area"],
rule=lambda state: state.has(laurels, player))
regions["Furnace Ladder Area"].connect(
connecting_region=regions["Furnace Walking Path"],
rule=lambda state: state.has(laurels, player))
regions["Furnace Walking Path"].connect(
connecting_region=regions["Furnace Fuse"],
rule=lambda state: state.has(laurels, player))
regions["Furnace Fuse"].connect(
connecting_region=regions["Furnace Walking Path"],
rule=lambda state: state.has(laurels, player))
regions["Furnace Fuse"].connect(
connecting_region=regions["Furnace Ladder Area"],
rule=lambda state: state.has(laurels, player))
regions["Furnace Ladder Area"].connect(
connecting_region=regions["Furnace Fuse"],
rule=lambda state: state.has(laurels, player))
# East Forest
regions["Forest Belltower Upper"].connect(
connecting_region=regions["Forest Belltower Main"])
regions["Forest Belltower Main"].connect(
connecting_region=regions["Forest Belltower Lower"])
# nmg: ice grapple up to dance fox spot, and vice versa
regions["East Forest"].connect(
connecting_region=regions["East Forest Dance Fox Spot"],
rule=lambda state: state.has(laurels, player)
or has_ice_grapple_logic(True, state, player, options, ability_unlocks))
regions["East Forest Dance Fox Spot"].connect(
connecting_region=regions["East Forest"],
rule=lambda state: state.has(laurels, player)
or has_ice_grapple_logic(True, state, player, options, ability_unlocks))
regions["East Forest"].connect(
connecting_region=regions["East Forest Portal"],
rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks))
regions["East Forest Portal"].connect(
connecting_region=regions["East Forest"])
regions["Guard House 1 East"].connect(
connecting_region=regions["Guard House 1 West"])
regions["Guard House 1 West"].connect(
connecting_region=regions["Guard House 1 East"],
rule=lambda state: state.has(laurels, player))
# nmg: ice grapple from upper grave path exit to the rest of it
regions["Forest Grave Path Upper"].connect(
connecting_region=regions["Forest Grave Path Main"],
rule=lambda state: state.has(laurels, player)
or has_ice_grapple_logic(True, state, player, options, ability_unlocks))
regions["Forest Grave Path Main"].connect(
connecting_region=regions["Forest Grave Path Upper"],
rule=lambda state: state.has(laurels, player))
regions["Forest Grave Path Main"].connect(
connecting_region=regions["Forest Grave Path by Grave"])
# nmg: ice grapple or laurels through the gate
regions["Forest Grave Path by Grave"].connect(
connecting_region=regions["Forest Grave Path Main"],
rule=lambda state: has_ice_grapple_logic(False, state, player, options, ability_unlocks)
or (state.has(laurels, player) and options.logic_rules))
regions["Forest Grave Path by Grave"].connect(
connecting_region=regions["Forest Hero's Grave"],
rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks))
regions["Forest Hero's Grave"].connect(
connecting_region=regions["Forest Grave Path by Grave"])
# Beneath the Well and Dark Tomb
regions["Beneath the Well Front"].connect(
connecting_region=regions["Beneath the Well Main"],
rule=lambda state: has_stick(state, player) or state.has(fire_wand, player))
regions["Beneath the Well Main"].connect(
connecting_region=regions["Beneath the Well Front"],
rule=lambda state: has_stick(state, player) or state.has(fire_wand, player))
regions["Beneath the Well Back"].connect(
connecting_region=regions["Beneath the Well Main"],
rule=lambda state: has_stick(state, player) or state.has(fire_wand, player))
regions["Beneath the Well Main"].connect(
connecting_region=regions["Beneath the Well Back"],
rule=lambda state: has_stick(state, player) or state.has(fire_wand, player))
regions["Well Boss"].connect(
connecting_region=regions["Dark Tomb Checkpoint"])
# nmg: can laurels through the gate
regions["Dark Tomb Checkpoint"].connect(
connecting_region=regions["Well Boss"],
rule=lambda state: state.has(laurels, player) and options.logic_rules)
regions["Dark Tomb Entry Point"].connect(
connecting_region=regions["Dark Tomb Main"],
rule=lambda state: has_lantern(state, player, options))
regions["Dark Tomb Main"].connect(
connecting_region=regions["Dark Tomb Entry Point"],
rule=lambda state: has_lantern(state, player, options))
regions["Dark Tomb Main"].connect(
connecting_region=regions["Dark Tomb Dark Exit"],
rule=lambda state: has_lantern(state, player, options))
regions["Dark Tomb Dark Exit"].connect(
connecting_region=regions["Dark Tomb Main"],
rule=lambda state: has_lantern(state, player, options))
# West Garden
regions["West Garden Laurels Exit"].connect(
connecting_region=regions["West Garden"],
rule=lambda state: state.has(laurels, player))
regions["West Garden"].connect(
connecting_region=regions["West Garden Laurels Exit"],
rule=lambda state: state.has(laurels, player))
# todo: can you wake the boss, then grapple to it, then kill it?
regions["West Garden after Boss"].connect(
connecting_region=regions["West Garden"],
rule=lambda state: state.has(laurels, player))
regions["West Garden"].connect(
connecting_region=regions["West Garden after Boss"],
rule=lambda state: state.has(laurels, player) or has_sword(state, player))
regions["West Garden"].connect(
connecting_region=regions["West Garden Hero's Grave"],
rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks))
regions["West Garden Hero's Grave"].connect(
connecting_region=regions["West Garden"])
regions["West Garden Portal"].connect(
connecting_region=regions["West Garden Portal Item"],
rule=lambda state: state.has(laurels, player))
regions["West Garden Portal Item"].connect(
connecting_region=regions["West Garden Portal"],
rule=lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks))
# nmg: can ice grapple to and from the item behind the magic dagger house
regions["West Garden Portal Item"].connect(
connecting_region=regions["West Garden"],
rule=lambda state: has_ice_grapple_logic(True, state, player, options, ability_unlocks))
regions["West Garden"].connect(
connecting_region=regions["West Garden Portal Item"],
rule=lambda state: has_ice_grapple_logic(True, state, player, options, ability_unlocks))
# Atoll and Frog's Domain
# nmg: ice grapple the bird below the portal
regions["Ruined Atoll"].connect(
connecting_region=regions["Ruined Atoll Lower Entry Area"],
rule=lambda state: state.has(laurels, player)
or has_ice_grapple_logic(True, state, player, options, ability_unlocks))
regions["Ruined Atoll Lower Entry Area"].connect(
connecting_region=regions["Ruined Atoll"],
rule=lambda state: state.has(laurels, player) or state.has(grapple, player))
regions["Ruined Atoll"].connect(
connecting_region=regions["Ruined Atoll Frog Mouth"],
rule=lambda state: state.has(laurels, player) or state.has(grapple, player))
regions["Ruined Atoll Frog Mouth"].connect(
connecting_region=regions["Ruined Atoll"],
rule=lambda state: state.has(laurels, player) or state.has(grapple, player))
regions["Ruined Atoll"].connect(
connecting_region=regions["Ruined Atoll Portal"],
rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks))
regions["Ruined Atoll Portal"].connect(
connecting_region=regions["Ruined Atoll"])
regions["Frog's Domain"].connect(
connecting_region=regions["Frog's Domain Back"],
rule=lambda state: state.has(grapple, player))
# Library
regions["Library Exterior Tree"].connect(
connecting_region=regions["Library Exterior Ladder"],
rule=lambda state: state.has(grapple, player) or state.has(laurels, player))
regions["Library Exterior Ladder"].connect(
connecting_region=regions["Library Exterior Tree"],
rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)
and (state.has(grapple, player) or state.has(laurels, player)))
regions["Library Hall"].connect(
connecting_region=regions["Library Hero's Grave"],
rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks))
regions["Library Hero's Grave"].connect(
connecting_region=regions["Library Hall"])
regions["Library Lab Lower"].connect(
connecting_region=regions["Library Lab"],
rule=lambda state: state.has(laurels, player) or state.has(grapple, player))
regions["Library Lab"].connect(
connecting_region=regions["Library Lab Lower"],
rule=lambda state: state.has(laurels, player))
regions["Library Lab"].connect(
connecting_region=regions["Library Portal"],
rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks))
regions["Library Portal"].connect(
connecting_region=regions["Library Lab"])
# Eastern Vault Fortress
regions["Fortress Exterior from East Forest"].connect(
connecting_region=regions["Fortress Exterior from Overworld"],
rule=lambda state: state.has(laurels, player) or state.has(grapple, player))
regions["Fortress Exterior from Overworld"].connect(
connecting_region=regions["Fortress Exterior from East Forest"],
rule=lambda state: state.has(laurels, player))
regions["Fortress Exterior near cave"].connect(
connecting_region=regions["Fortress Exterior from Overworld"],
rule=lambda state: state.has(laurels, player))
regions["Fortress Exterior from Overworld"].connect(
connecting_region=regions["Fortress Exterior near cave"],
rule=lambda state: state.has(laurels, player) or has_ability(state, player, prayer, options, ability_unlocks))
regions["Fortress Courtyard"].connect(
connecting_region=regions["Fortress Exterior from Overworld"],
rule=lambda state: state.has(laurels, player))
# nmg: can ice grapple an enemy in the courtyard
regions["Fortress Exterior from Overworld"].connect(
connecting_region=regions["Fortress Courtyard"],
rule=lambda state: state.has(laurels, player)
or has_ice_grapple_logic(True, state, player, options, ability_unlocks))
regions["Fortress Courtyard Upper"].connect(
connecting_region=regions["Fortress Courtyard"])
# nmg: can ice grapple to the upper ledge
regions["Fortress Courtyard"].connect(
connecting_region=regions["Fortress Courtyard Upper"],
rule=lambda state: has_ice_grapple_logic(True, state, player, options, ability_unlocks))
regions["Fortress Courtyard Upper"].connect(
connecting_region=regions["Fortress Exterior from Overworld"])
regions["Beneath the Vault Front"].connect(
connecting_region=regions["Beneath the Vault Back"],
rule=lambda state: has_lantern(state, player, options))
regions["Beneath the Vault Back"].connect(
connecting_region=regions["Beneath the Vault Front"])
regions["Fortress East Shortcut Upper"].connect(
connecting_region=regions["Fortress East Shortcut Lower"])
# nmg: can ice grapple upwards
regions["Fortress East Shortcut Lower"].connect(
connecting_region=regions["Fortress East Shortcut Upper"],
rule=lambda state: has_ice_grapple_logic(True, state, player, options, ability_unlocks))
# nmg: ice grapple through the big gold door, can do it both ways
regions["Eastern Vault Fortress"].connect(
connecting_region=regions["Eastern Vault Fortress Gold Door"],
name="Fortress Gold Door",
rule=lambda state: state.has_all({"Activate Eastern Vault West Fuses",
"Activate Eastern Vault East Fuse"}, player)
or has_ice_grapple_logic(False, state, player, options, ability_unlocks))
regions["Eastern Vault Fortress Gold Door"].connect(
connecting_region=regions["Eastern Vault Fortress"],
name="Fortress Gold Door",
rule=lambda state: has_ice_grapple_logic(True, state, player, options, ability_unlocks))
regions["Fortress Grave Path"].connect(
connecting_region=regions["Fortress Grave Path Dusty Entrance"],
rule=lambda state: state.has(laurels, player))
regions["Fortress Grave Path Dusty Entrance"].connect(
connecting_region=regions["Fortress Grave Path"],
rule=lambda state: state.has(laurels, player))
regions["Fortress Grave Path"].connect(
connecting_region=regions["Fortress Hero's Grave"],
rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks))
regions["Fortress Hero's Grave"].connect(
connecting_region=regions["Fortress Grave Path"])
# nmg: ice grapple from upper grave path to lower
regions["Fortress Grave Path Upper"].connect(
connecting_region=regions["Fortress Grave Path"],
rule=lambda state: has_ice_grapple_logic(True, state, player, options, ability_unlocks))
regions["Fortress Arena"].connect(
connecting_region=regions["Fortress Arena Portal"],
name="Fortress Arena to Fortress Portal",
rule=lambda state: state.has("Activate Eastern Vault West Fuses", player))
regions["Fortress Arena Portal"].connect(
connecting_region=regions["Fortress Arena"])
# Quarry
regions["Lower Mountain"].connect(
connecting_region=regions["Lower Mountain Stairs"],
rule=lambda state: has_ability(state, player, holy_cross, options, ability_unlocks))
regions["Lower Mountain Stairs"].connect(
connecting_region=regions["Lower Mountain"],
rule=lambda state: has_ability(state, player, holy_cross, options, ability_unlocks))
regions["Quarry Entry"].connect(
connecting_region=regions["Quarry Portal"],
name="Quarry to Quarry Portal",
rule=lambda state: state.has("Activate Quarry Fuse", player))
regions["Quarry Portal"].connect(
connecting_region=regions["Quarry Entry"])
regions["Quarry Entry"].connect(
connecting_region=regions["Quarry"],
rule=lambda state: state.has(fire_wand, player) or has_sword(state, player))
regions["Quarry"].connect(
connecting_region=regions["Quarry Entry"])
regions["Quarry Back"].connect(
connecting_region=regions["Quarry"],
rule=lambda state: state.has(fire_wand, player) or has_sword(state, player))
regions["Quarry"].connect(
connecting_region=regions["Quarry Back"])
regions["Quarry Monastery Entry"].connect(
connecting_region=regions["Quarry"],
rule=lambda state: state.has(fire_wand, player) or has_sword(state, player))
regions["Quarry"].connect(
connecting_region=regions["Quarry Monastery Entry"])
regions["Monastery Rope"].connect(
connecting_region=regions["Quarry Back"])
regions["Quarry"].connect(
connecting_region=regions["Lower Quarry"],
rule=lambda state: has_mask(state, player, options))
# nmg: bring a scav over, then ice grapple through the door
regions["Lower Quarry"].connect(
connecting_region=regions["Lower Quarry Zig Door"],
name="Quarry to Zig Door",
rule=lambda state: state.has("Activate Quarry Fuse", player)
or has_ice_grapple_logic(False, state, player, options, ability_unlocks))
# nmg: use ice grapple to get from the beginning of Quarry to the door without really needing mask
regions["Quarry"].connect(
connecting_region=regions["Lower Quarry Zig Door"],
rule=lambda state: has_ice_grapple_logic(True, state, player, options, ability_unlocks))
regions["Monastery Front"].connect(
connecting_region=regions["Monastery Back"])
# nmg: can laurels through the gate
regions["Monastery Back"].connect(
connecting_region=regions["Monastery Front"],
rule=lambda state: state.has(laurels, player) and options.logic_rules)
regions["Monastery Back"].connect(
connecting_region=regions["Monastery Hero's Grave"],
rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks))
regions["Monastery Hero's Grave"].connect(
connecting_region=regions["Monastery Back"])
# Ziggurat
regions["Rooted Ziggurat Upper Entry"].connect(
connecting_region=regions["Rooted Ziggurat Upper Front"])
regions["Rooted Ziggurat Upper Front"].connect(
connecting_region=regions["Rooted Ziggurat Upper Back"],
rule=lambda state: state.has(laurels, player) or has_sword(state, player))
regions["Rooted Ziggurat Upper Back"].connect(
connecting_region=regions["Rooted Ziggurat Upper Front"],
rule=lambda state: state.has(laurels, player))
regions["Rooted Ziggurat Middle Top"].connect(
connecting_region=regions["Rooted Ziggurat Middle Bottom"])
regions["Rooted Ziggurat Lower Front"].connect(
connecting_region=regions["Rooted Ziggurat Lower Back"],
rule=lambda state: state.has(laurels, player)
or (has_sword(state, player) and has_ability(state, player, prayer, options, ability_unlocks)))
# unrestricted: use ladder storage to get to the front, get hit by one of the many enemies
regions["Rooted Ziggurat Lower Back"].connect(
connecting_region=regions["Rooted Ziggurat Lower Front"],
rule=lambda state: state.has(laurels, player) or can_ladder_storage(state, player, options))
regions["Rooted Ziggurat Lower Back"].connect(
connecting_region=regions["Rooted Ziggurat Portal Room Entrance"],
rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks))
regions["Rooted Ziggurat Portal Room Entrance"].connect(
connecting_region=regions["Rooted Ziggurat Lower Back"])
regions["Rooted Ziggurat Portal"].connect(
connecting_region=regions["Rooted Ziggurat Portal Room Exit"],
name="Zig Portal Room Exit",
rule=lambda state: state.has("Activate Ziggurat Fuse", player))
regions["Rooted Ziggurat Portal Room Exit"].connect(
connecting_region=regions["Rooted Ziggurat Portal"],
rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks))
# Swamp and Cathedral
# nmg: ice grapple through cathedral door, can do it both ways
regions["Swamp"].connect(
connecting_region=regions["Swamp to Cathedral Main Entrance"],
rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks)
or has_ice_grapple_logic(False, state, player, options, ability_unlocks))
regions["Swamp to Cathedral Main Entrance"].connect(
connecting_region=regions["Swamp"],
rule=lambda state: has_ice_grapple_logic(False, state, player, options, ability_unlocks))
regions["Swamp"].connect(
connecting_region=regions["Swamp to Cathedral Treasure Room"],
rule=lambda state: has_ability(state, player, holy_cross, options, ability_unlocks))
regions["Swamp to Cathedral Treasure Room"].connect(
connecting_region=regions["Swamp"])
regions["Back of Swamp"].connect(
connecting_region=regions["Back of Swamp Laurels Area"],
rule=lambda state: state.has(laurels, player))
regions["Back of Swamp Laurels Area"].connect(
connecting_region=regions["Back of Swamp"],
rule=lambda state: state.has(laurels, player))
# nmg: can ice grapple down while you're on the pillars
regions["Back of Swamp Laurels Area"].connect(
connecting_region=regions["Swamp"],
rule=lambda state: state.has(laurels, player)
and has_ice_grapple_logic(True, state, player, options, ability_unlocks))
regions["Back of Swamp"].connect(
connecting_region=regions["Swamp Hero's Grave"],
rule=lambda state: has_ability(state, player, prayer, options, ability_unlocks))
regions["Swamp Hero's Grave"].connect(
connecting_region=regions["Back of Swamp"])
regions["Cathedral Gauntlet Checkpoint"].connect(
connecting_region=regions["Cathedral Gauntlet"])
regions["Cathedral Gauntlet"].connect(
connecting_region=regions["Cathedral Gauntlet Exit"],
rule=lambda state: state.has(laurels, player))
regions["Cathedral Gauntlet Exit"].connect(
connecting_region=regions["Cathedral Gauntlet"],
rule=lambda state: state.has(laurels, player))
# Far Shore
regions["Far Shore"].connect(
connecting_region=regions["Far Shore to Spawn"],
rule=lambda state: state.has(laurels, player))
regions["Far Shore to Spawn"].connect(
connecting_region=regions["Far Shore"],
rule=lambda state: state.has(laurels, player))
regions["Far Shore"].connect(
connecting_region=regions["Far Shore to East Forest"],
rule=lambda state: state.has(laurels, player))
regions["Far Shore to East Forest"].connect(
connecting_region=regions["Far Shore"],
rule=lambda state: state.has(laurels, player))
regions["Far Shore"].connect(
connecting_region=regions["Far Shore to West Garden"],
name="Far Shore to West Garden",
rule=lambda state: state.has("Activate West Garden Fuse", player))
regions["Far Shore to West Garden"].connect(
connecting_region=regions["Far Shore"])
regions["Far Shore"].connect(
connecting_region=regions["Far Shore to Quarry"],
name="Far Shore to Quarry",
rule=lambda state: state.has("Activate Quarry Fuse", player))
regions["Far Shore to Quarry"].connect(
connecting_region=regions["Far Shore"])
regions["Far Shore"].connect(
connecting_region=regions["Far Shore to Fortress"],
name="Far Shore to Fortress",
rule=lambda state: state.has("Activate Eastern Vault West Fuses", player))
regions["Far Shore to Fortress"].connect(
connecting_region=regions["Far Shore"])
regions["Far Shore"].connect(
connecting_region=regions["Far Shore to Library"],
name="Far Shore to Library",
rule=lambda state: state.has("Activate Library Fuse", player))
regions["Far Shore to Library"].connect(
connecting_region=regions["Far Shore"])
# Misc
regions["Shop Entrance 1"].connect(
connecting_region=regions["Shop"])
regions["Shop Entrance 2"].connect(
connecting_region=regions["Shop"])
regions["Shop Entrance 3"].connect(
connecting_region=regions["Shop"])
regions["Shop Entrance 4"].connect(
connecting_region=regions["Shop"])
regions["Shop Entrance 5"].connect(
connecting_region=regions["Shop"])
regions["Shop Entrance 6"].connect(
connecting_region=regions["Shop"])
regions["Spirit Arena"].connect(
connecting_region=regions["Spirit Arena Victory"],
rule=lambda state: (state.has(gold_hexagon, player, world.options.hexagon_goal.value) if
world.options.hexagon_quest else
state.has_all({red_hexagon, green_hexagon, blue_hexagon}, player)))
# connecting the regions portals are in to other portals you can access via ladder storage
# using has_stick instead of can_ladder_storage since it's already checking the logic rules
if options.logic_rules == "unrestricted":
def get_paired_region(portal_sd: str) -> str:
for portal1, portal2 in portal_pairs.items():
if portal1.scene_destination() == portal_sd:
return portal2.region
if portal2.scene_destination() == portal_sd:
return portal1.region
raise Exception("no matches found in get_paired_region")
# The upper Swamp entrance
regions["Overworld"].connect(
regions[get_paired_region("Overworld Redux, Swamp Redux 2_wall")],
rule=lambda state: has_stick(state, player))
# Western Furnace entrance, next to the sign that leads to West Garden
regions["Overworld"].connect(
regions[get_paired_region("Overworld Redux, Furnace_gyro_west")],
rule=lambda state: has_stick(state, player))
# Upper West Garden entry, by the belltower
regions["Overworld"].connect(
regions[get_paired_region("Overworld Redux, Archipelagos Redux_upper")],
rule=lambda state: has_stick(state, player))
# West Garden entry by the Furnace
regions["Overworld"].connect(
regions[get_paired_region("Overworld Redux, Archipelagos Redux_lower")],
rule=lambda state: has_stick(state, player))
# West Garden laurels entrance, by the beach
regions["Overworld"].connect(
regions[get_paired_region("Overworld Redux, Archipelagos Redux_lowest")],
rule=lambda state: has_stick(state, player))
# Well rail, west side. Can ls in town, get extra height by going over the portal pad
regions["Overworld"].connect(
regions[get_paired_region("Overworld Redux, Sewer_west_aqueduct")],
rule=lambda state: has_stick(state, player))
# Well rail, east side. Need some height from the temple stairs
regions["Overworld"].connect(
regions[get_paired_region("Overworld Redux, Furnace_gyro_upper_north")],
rule=lambda state: has_stick(state, player))
# Furnace ladder to the fuse entrance
regions["Furnace Ladder Area"].connect(
regions[get_paired_region("Furnace, Overworld Redux_gyro_upper_north")],
rule=lambda state: has_stick(state, player))
# Furnace ladder to Dark Tomb
regions["Furnace Ladder Area"].connect(
regions[get_paired_region("Furnace, Crypt Redux_")],
rule=lambda state: has_stick(state, player))
# Furnace ladder to the West Garden connector
regions["Furnace Ladder Area"].connect(
regions[get_paired_region("Furnace, Overworld Redux_gyro_west")],
rule=lambda state: has_stick(state, player))
# West Garden exit after Garden Knight
regions["West Garden"].connect(
regions[get_paired_region("Archipelagos Redux, Overworld Redux_upper")],
rule=lambda state: has_stick(state, player))
# West Garden laurels exit
regions["West Garden"].connect(
regions[get_paired_region("Archipelagos Redux, Overworld Redux_lowest")],
rule=lambda state: has_stick(state, player))
# Frog mouth entrance
regions["Ruined Atoll"].connect(
regions[get_paired_region("Atoll Redux, Frog Stairs_mouth")],
rule=lambda state: has_stick(state, player))
# Entrance by the dancing fox holy cross spot
regions["East Forest"].connect(
regions[get_paired_region("East Forest Redux, East Forest Redux Laddercave_upper")],
rule=lambda state: has_stick(state, player))
# From the west side of guard house 1 to the east side
regions["Guard House 1 West"].connect(
regions[get_paired_region("East Forest Redux Laddercave, East Forest Redux_gate")],
rule=lambda state: has_stick(state, player))
regions["Guard House 1 West"].connect(
regions[get_paired_region("East Forest Redux Laddercave, Forest Boss Room_")],
rule=lambda state: has_stick(state, player))
# Upper exit from the Forest Grave Path, use ls at the ladder by the gate switch
regions["Forest Grave Path Main"].connect(
regions[get_paired_region("Sword Access, East Forest Redux_upper")],
rule=lambda state: has_stick(state, player))
# Fortress exterior shop, ls at the ladder by the telescope
regions["Fortress Exterior from Overworld"].connect(
regions[get_paired_region("Fortress Courtyard, Shop_")],
rule=lambda state: has_stick(state, player))
# Fortress main entry and grave path lower entry, ls at the ladder by the telescope
regions["Fortress Exterior from Overworld"].connect(
regions[get_paired_region("Fortress Courtyard, Fortress Main_Big Door")],
rule=lambda state: has_stick(state, player))
regions["Fortress Exterior from Overworld"].connect(
regions[get_paired_region("Fortress Courtyard, Fortress Reliquary_Lower")],
rule=lambda state: has_stick(state, player))
# Upper exits from the courtyard. Use the ramp in the courtyard, then the blocks north of the first fuse
regions["Fortress Exterior from Overworld"].connect(
regions[get_paired_region("Fortress Courtyard, Fortress Reliquary_Upper")],
rule=lambda state: has_stick(state, player))
regions["Fortress Exterior from Overworld"].connect(
regions[get_paired_region("Fortress Courtyard, Fortress East_")],
rule=lambda state: has_stick(state, player))
# same as above, except from the east side of the area
regions["Fortress Exterior from East Forest"].connect(
regions[get_paired_region("Fortress Courtyard, Overworld Redux_")],
rule=lambda state: has_stick(state, player))
regions["Fortress Exterior from East Forest"].connect(
regions[get_paired_region("Fortress Courtyard, Shop_")],
rule=lambda state: has_stick(state, player))
regions["Fortress Exterior from East Forest"].connect(
regions[get_paired_region("Fortress Courtyard, Fortress Main_Big Door")],
rule=lambda state: has_stick(state, player))
regions["Fortress Exterior from East Forest"].connect(
regions[get_paired_region("Fortress Courtyard, Fortress Reliquary_Lower")],
rule=lambda state: has_stick(state, player))
regions["Fortress Exterior from East Forest"].connect(
regions[get_paired_region("Fortress Courtyard, Fortress Reliquary_Upper")],
rule=lambda state: has_stick(state, player))
regions["Fortress Exterior from East Forest"].connect(
regions[get_paired_region("Fortress Courtyard, Fortress East_")],
rule=lambda state: has_stick(state, player))
# same as above, except from the Beneath the Vault entrance ladder
regions["Fortress Exterior near cave"].connect(
regions[get_paired_region("Fortress Courtyard, Overworld Redux_")],
rule=lambda state: has_stick(state, player))
regions["Fortress Exterior near cave"].connect(
regions[get_paired_region("Fortress Courtyard, Fortress Main_Big Door")],
rule=lambda state: has_stick(state, player))
regions["Fortress Exterior near cave"].connect(
regions[get_paired_region("Fortress Courtyard, Fortress Reliquary_Lower")],
rule=lambda state: has_stick(state, player))
regions["Fortress Exterior near cave"].connect(
regions[get_paired_region("Fortress Courtyard, Fortress Reliquary_Upper")],
rule=lambda state: has_stick(state, player))
regions["Fortress Exterior near cave"].connect(
regions[get_paired_region("Fortress Courtyard, Fortress East_")],
rule=lambda state: has_stick(state, player))
# ls at the ladder, need to gain a little height to get up the stairs
regions["Lower Mountain"].connect(
regions[get_paired_region("Mountain, Mountaintop_")],
rule=lambda state: has_stick(state, player))
# Where the rope is behind Monastery. Connecting here since, if you have this region, you don't need a sword
regions["Quarry Monastery Entry"].connect(
regions[get_paired_region("Quarry Redux, Monastery_back")],
rule=lambda state: has_stick(state, player))
# Swamp to Gauntlet
regions["Swamp"].connect(
regions[get_paired_region("Swamp Redux 2, Cathedral Arena_")],
rule=lambda state: has_stick(state, player))
# Swamp to Overworld upper
regions["Swamp"].connect(
regions[get_paired_region("Swamp Redux 2, Overworld Redux_wall")],
rule=lambda state: has_stick(state, player))
# Ladder by the hero grave
regions["Back of Swamp"].connect(
regions[get_paired_region("Swamp Redux 2, Overworld Redux_conduit")],
rule=lambda state: has_stick(state, player))
regions["Back of Swamp"].connect(
regions[get_paired_region("Swamp Redux 2, Shop_")],
rule=lambda state: has_stick(state, player))
# Need to put the cathedral HC code mid-flight
regions["Back of Swamp"].connect(
regions[get_paired_region("Swamp Redux 2, Cathedral Redux_secret")],
rule=lambda state: has_stick(state, player)
and has_ability(state, player, holy_cross, options, ability_unlocks))
def set_er_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) -> None:
player = world.player
multiworld = world.multiworld
options = world.options
forbid_item(multiworld.get_location("Secret Gathering Place - 20 Fairy Reward", player), fairies, player)
# Ability Shuffle Exclusive Rules
set_rule(multiworld.get_location("East Forest - Dancing Fox Spirit Holy Cross", player),
lambda state: has_ability(state, player, holy_cross, options, ability_unlocks))
set_rule(multiworld.get_location("Forest Grave Path - Holy Cross Code by Grave", player),
lambda state: has_ability(state, player, holy_cross, options, ability_unlocks))
set_rule(multiworld.get_location("East Forest - Golden Obelisk Holy Cross", player),
lambda state: has_ability(state, player, holy_cross, options, ability_unlocks))
set_rule(multiworld.get_location("Beneath the Well - [Powered Secret Room] Chest", player),
lambda state: state.has("Activate Furnace Fuse", player))
set_rule(multiworld.get_location("West Garden - [North] Behind Holy Cross Door", player),
lambda state: has_ability(state, player, holy_cross, options, ability_unlocks))
set_rule(multiworld.get_location("Library Hall - Holy Cross Chest", player),
lambda state: has_ability(state, player, holy_cross, options, ability_unlocks))
set_rule(multiworld.get_location("Eastern Vault Fortress - [West Wing] Candles Holy Cross", player),
lambda state: has_ability(state, player, holy_cross, options, ability_unlocks))
set_rule(multiworld.get_location("West Garden - [Central Highlands] Holy Cross (Blue Lines)", player),
lambda state: has_ability(state, player, holy_cross, options, ability_unlocks))
set_rule(multiworld.get_location("Quarry - [Back Entrance] Bushes Holy Cross", player),
lambda state: has_ability(state, player, holy_cross, options, ability_unlocks))
set_rule(multiworld.get_location("Cathedral - Secret Legend Trophy Chest", player),
lambda state: has_ability(state, player, holy_cross, options, ability_unlocks))
# Overworld
set_rule(multiworld.get_location("Overworld - [Southwest] Grapple Chest Over Walkway", player),
lambda state: state.has_any({grapple, laurels}, player))
set_rule(multiworld.get_location("Overworld - [Southwest] West Beach Guarded By Turret 2", player),
lambda state: state.has_any({grapple, laurels}, player))
set_rule(multiworld.get_location("Overworld - [Southwest] From West Garden", player),
lambda state: state.has(laurels, player))
set_rule(multiworld.get_location("Overworld - [Southeast] Page on Pillar by Swamp", player),
lambda state: state.has(laurels, player))
set_rule(multiworld.get_location("Overworld - [Southwest] Fountain Page", player),
lambda state: state.has(laurels, player))
set_rule(multiworld.get_location("Overworld - [Northwest] Page on Pillar by Dark Tomb", player),
lambda state: state.has(laurels, player))
set_rule(multiworld.get_location("Old House - Holy Cross Chest", player),
lambda state: has_ability(state, player, holy_cross, options, ability_unlocks))
set_rule(multiworld.get_location("Overworld - [East] Grapple Chest", player),
lambda state: state.has(grapple, player))
set_rule(multiworld.get_location("Sealed Temple - Holy Cross Chest", player),
lambda state: has_ability(state, player, holy_cross, options, ability_unlocks))
set_rule(multiworld.get_location("Caustic Light Cave - Holy Cross Chest", player),
lambda state: has_ability(state, player, holy_cross, options, ability_unlocks))
set_rule(multiworld.get_location("Cube Cave - Holy Cross Chest", player),
lambda state: has_ability(state, player, holy_cross, options, ability_unlocks))
set_rule(multiworld.get_location("Old House - Holy Cross Door Page", player),
lambda state: has_ability(state, player, holy_cross, options, ability_unlocks))
set_rule(multiworld.get_location("Maze Cave - Maze Room Holy Cross", player),
lambda state: has_ability(state, player, holy_cross, options, ability_unlocks))
set_rule(multiworld.get_location("Old House - Holy Cross Chest", player),
lambda state: has_ability(state, player, holy_cross, options, ability_unlocks))
set_rule(multiworld.get_location("Patrol Cave - Holy Cross Chest", player),
lambda state: has_ability(state, player, holy_cross, options, ability_unlocks))
set_rule(multiworld.get_location("Ruined Passage - Holy Cross Chest", player),
lambda state: has_ability(state, player, holy_cross, options, ability_unlocks))
set_rule(multiworld.get_location("Hourglass Cave - Holy Cross Chest", player),
lambda state: has_ability(state, player, holy_cross, options, ability_unlocks))
set_rule(multiworld.get_location("Secret Gathering Place - Holy Cross Chest", player),
lambda state: has_ability(state, player, holy_cross, options, ability_unlocks))
set_rule(multiworld.get_location("Secret Gathering Place - 10 Fairy Reward", player),
lambda state: state.has(fairies, player, 10))
set_rule(multiworld.get_location("Secret Gathering Place - 20 Fairy Reward", player),
lambda state: state.has(fairies, player, 20))
set_rule(multiworld.get_location("Coins in the Well - 3 Coins", player), lambda state: state.has(coins, player, 3))
set_rule(multiworld.get_location("Coins in the Well - 6 Coins", player), lambda state: state.has(coins, player, 6))
set_rule(multiworld.get_location("Coins in the Well - 10 Coins", player),
lambda state: state.has(coins, player, 10))
set_rule(multiworld.get_location("Coins in the Well - 15 Coins", player),
lambda state: state.has(coins, player, 15))
# East Forest
set_rule(multiworld.get_location("East Forest - Lower Grapple Chest", player),
lambda state: state.has(grapple, player))
set_rule(multiworld.get_location("East Forest - Lower Dash Chest", player),
lambda state: state.has_all({grapple, laurels}, player))
set_rule(multiworld.get_location("East Forest - Ice Rod Grapple Chest", player), lambda state: (
state.has_all({grapple, ice_dagger, fire_wand}, player) and
has_ability(state, player, ice_rod, options, ability_unlocks)))
# West Garden
set_rule(multiworld.get_location("West Garden - [North] Across From Page Pickup", player),
lambda state: state.has(laurels, player))
set_rule(multiworld.get_location("West Garden - [West] In Flooded Walkway", player),
lambda state: state.has(laurels, player))
set_rule(multiworld.get_location("West Garden - [West Lowlands] Tree Holy Cross Chest", player),
lambda state: state.has(laurels, player) and has_ability(state, player, holy_cross, options,
ability_unlocks))
set_rule(multiworld.get_location("West Garden - [East Lowlands] Page Behind Ice Dagger House", player),
lambda state: state.has(laurels, player))
set_rule(multiworld.get_location("West Garden - [Central Lowlands] Below Left Walkway", player),
lambda state: state.has(laurels, player))
# Ruined Atoll
set_rule(multiworld.get_location("Ruined Atoll - [West] Near Kevin Block", player),
lambda state: state.has(laurels, player))
set_rule(multiworld.get_location("Ruined Atoll - [East] Locked Room Lower Chest", player),
lambda state: state.has_any({laurels, key}, player))
set_rule(multiworld.get_location("Ruined Atoll - [East] Locked Room Upper Chest", player),
lambda state: state.has_any({laurels, key}, player))
# Frog's Domain
set_rule(multiworld.get_location("Frog's Domain - Side Room Grapple Secret", player),
lambda state: state.has_any({grapple, laurels}, player))
set_rule(multiworld.get_location("Frog's Domain - Grapple Above Hot Tub", player),
lambda state: state.has_any({grapple, laurels}, player))
set_rule(multiworld.get_location("Frog's Domain - Escape Chest", player),
lambda state: state.has_any({grapple, laurels}, player))
# Eastern Vault Fortress
set_rule(multiworld.get_location("Fortress Arena - Hexagon Red", player),
lambda state: state.has(vault_key, player))
# Beneath the Vault
set_rule(multiworld.get_location("Beneath the Fortress - Bridge", player),
lambda state: state.has_group("melee weapons", player, 1) or state.has_any({laurels, fire_wand}, player))
set_rule(multiworld.get_location("Beneath the Fortress - Obscured Behind Waterfall", player),
lambda state: has_lantern(state, player, options))
# Quarry
set_rule(multiworld.get_location("Quarry - [Central] Above Ladder Dash Chest", player),
lambda state: state.has(laurels, player))
set_rule(multiworld.get_location("Quarry - [West] Upper Area Bombable Wall", player),
lambda state: has_mask(state, player, options))
# Ziggurat
set_rule(multiworld.get_location("Rooted Ziggurat Upper - Near Bridge Switch", player),
lambda state: has_sword(state, player) or state.has(fire_wand, player))
set_rule(multiworld.get_location("Rooted Ziggurat Lower - After Guarded Fuse", player),
lambda state: has_sword(state, player) and has_ability(state, player, prayer, options, ability_unlocks))
# Bosses
set_rule(multiworld.get_location("Fortress Arena - Siege Engine/Vault Key Pickup", player),
lambda state: has_sword(state, player))
set_rule(multiworld.get_location("Librarian - Hexagon Green", player),
lambda state: has_sword(state, player))
set_rule(multiworld.get_location("Rooted Ziggurat Lower - Hexagon Blue", player),
lambda state: has_sword(state, player))
# Swamp
set_rule(multiworld.get_location("Cathedral Gauntlet - Gauntlet Reward", player),
lambda state: state.has(fire_wand, player) and has_sword(state, player))
set_rule(multiworld.get_location("Swamp - [Entrance] Above Entryway", player),
lambda state: state.has(laurels, player))
set_rule(multiworld.get_location("Swamp - [South Graveyard] Upper Walkway Dash Chest", player),
lambda state: state.has(laurels, player))
# these two swamp checks really want you to kill the big skeleton first
set_rule(multiworld.get_location("Swamp - [South Graveyard] 4 Orange Skulls", player),
lambda state: has_sword(state, player))
set_rule(multiworld.get_location("Swamp - [South Graveyard] Guarded By Tentacles", player),
lambda state: has_sword(state, player))
# Hero's Grave and Far Shore
set_rule(multiworld.get_location("Hero's Grave - Tooth Relic", player),
lambda state: state.has(laurels, player))
set_rule(multiworld.get_location("Hero's Grave - Mushroom Relic", player),
lambda state: state.has(laurels, player))
set_rule(multiworld.get_location("Hero's Grave - Ash Relic", player),
lambda state: state.has(laurels, player))
set_rule(multiworld.get_location("Hero's Grave - Flowers Relic", player),
lambda state: state.has(laurels, player))
set_rule(multiworld.get_location("Hero's Grave - Effigy Relic", player),
lambda state: state.has(laurels, player))
set_rule(multiworld.get_location("Hero's Grave - Feathers Relic", player),
lambda state: state.has(laurels, player))
set_rule(multiworld.get_location("Far Shore - Secret Chest", player),
lambda state: state.has(laurels, player))
# Events
set_rule(multiworld.get_location("Eastern Bell", player),
lambda state: (has_stick(state, player) or state.has(fire_wand, player)))
set_rule(multiworld.get_location("Western Bell", player),
lambda state: (has_stick(state, player) or state.has(fire_wand, player)))
set_rule(multiworld.get_location("Furnace Fuse", player),
lambda state: has_ability(state, player, prayer, options, ability_unlocks))
set_rule(multiworld.get_location("South and West Fortress Exterior Fuses", player),
lambda state: has_ability(state, player, prayer, options, ability_unlocks))
set_rule(multiworld.get_location("Upper and Central Fortress Exterior Fuses", player),
lambda state: has_ability(state, player, prayer, options, ability_unlocks))
set_rule(multiworld.get_location("Beneath the Vault Fuse", player),
lambda state: state.has("Activate South and West Fortress Exterior Fuses", player))
set_rule(multiworld.get_location("Eastern Vault West Fuses", player),
lambda state: state.has("Activate Beneath the Vault Fuse", player))
set_rule(multiworld.get_location("Eastern Vault East Fuse", player),
lambda state: state.has_all({"Activate Upper and Central Fortress Exterior Fuses",
"Activate South and West Fortress Exterior Fuses"}, player))
set_rule(multiworld.get_location("Quarry Connector Fuse", player),
lambda state: has_ability(state, player, prayer, options, ability_unlocks) and state.has(grapple, player))
set_rule(multiworld.get_location("Quarry Fuse", player),
lambda state: state.has("Activate Quarry Connector Fuse", player))
set_rule(multiworld.get_location("Ziggurat Fuse", player),
lambda state: has_ability(state, player, prayer, options, ability_unlocks))
set_rule(multiworld.get_location("West Garden Fuse", player),
lambda state: has_ability(state, player, prayer, options, ability_unlocks))
set_rule(multiworld.get_location("Library Fuse", player),
lambda state: has_ability(state, player, prayer, options, ability_unlocks))

453
worlds/tunic/er_scripts.py Normal file
View File

@@ -0,0 +1,453 @@
from typing import Dict, List, Set, Tuple, TYPE_CHECKING
from BaseClasses import Region, ItemClassification, Item, Location
from .locations import location_table
from .er_data import Portal, tunic_er_regions, portal_mapping, hallway_helper, hallway_helper_nmg, \
dependent_regions, dependent_regions_nmg, dependent_regions_ur
from .er_rules import set_er_region_rules
if TYPE_CHECKING:
from . import TunicWorld
class TunicERItem(Item):
game: str = "Tunic"
class TunicERLocation(Location):
game: str = "Tunic"
def create_er_regions(world: "TunicWorld") -> Tuple[Dict[Portal, Portal], Dict[int, str]]:
regions: Dict[str, Region] = {}
portal_pairs: Dict[Portal, Portal] = pair_portals(world)
logic_rules = world.options.logic_rules
# check if a portal leads to a hallway. if it does, update the hint text accordingly
def hint_helper(portal: Portal, hint_string: str = "") -> str:
# start by setting it as the name of the portal, for the case we're not using the hallway helper
if hint_string == "":
hint_string = portal.name
if logic_rules:
hallways = hallway_helper_nmg
else:
hallways = hallway_helper
if portal.scene_destination() in hallways:
# if we have a hallway, we want the region rather than the portal name
if hint_string == portal.name:
hint_string = portal.region
# library exterior is two regions, we just want to fix up the name
if hint_string in {"Library Exterior Tree", "Library Exterior Ladder"}:
hint_string = "Library Exterior"
# search through the list for the other end of the hallway
for portala, portalb in portal_pairs.items():
if portala.scene_destination() == hallways[portal.scene_destination()]:
# if we find that we have a chain of hallways, do recursion
if portalb.scene_destination() in hallways:
hint_region = portalb.region
if hint_region in {"Library Exterior Tree", "Library Exterior Ladder"}:
hint_region = "Library Exterior"
hint_string = hint_region + " then " + hint_string
hint_string = hint_helper(portalb, hint_string)
else:
# if we didn't find a chain, get the portal name for the end of the chain
hint_string = portalb.name + " then " + hint_string
return hint_string
# and then the same thing for the other portal, since we have to check each separately
if portalb.scene_destination() == hallways[portal.scene_destination()]:
if portala.scene_destination() in hallways:
hint_region = portala.region
if hint_region in {"Library Exterior Tree", "Library Exterior Ladder"}:
hint_region = "Library Exterior"
hint_string = hint_region + " then " + hint_string
hint_string = hint_helper(portala, hint_string)
else:
hint_string = portala.name + " then " + hint_string
return hint_string
return hint_string
# create our regions, give them hint text if they're in a spot where it makes sense to
for region_name, region_data in tunic_er_regions.items():
hint_text = "error"
if region_data.hint == 1:
for portal1, portal2 in portal_pairs.items():
if portal1.region == region_name:
hint_text = hint_helper(portal2)
break
if portal2.region == region_name:
hint_text = hint_helper(portal1)
break
regions[region_name] = Region(region_name, world.player, world.multiworld, hint_text)
elif region_data.hint == 2:
for portal1, portal2 in portal_pairs.items():
if portal1.scene() == tunic_er_regions[region_name].game_scene:
hint_text = hint_helper(portal2)
break
if portal2.scene() == tunic_er_regions[region_name].game_scene:
hint_text = hint_helper(portal1)
break
regions[region_name] = Region(region_name, world.player, world.multiworld, hint_text)
elif region_data.hint == 3:
# only the west garden portal item for now
if region_name == "West Garden Portal Item":
if world.options.logic_rules:
for portal1, portal2 in portal_pairs.items():
if portal1.scene() == "Archipelagos Redux":
hint_text = hint_helper(portal2)
break
if portal2.scene() == "Archipelagos Redux":
hint_text = hint_helper(portal1)
break
regions[region_name] = Region(region_name, world.player, world.multiworld, hint_text)
else:
for portal1, portal2 in portal_pairs.items():
if portal1.region == "West Garden Portal":
hint_text = hint_helper(portal2)
break
if portal2.region == "West Garden Portal":
hint_text = hint_helper(portal1)
break
regions[region_name] = Region(region_name, world.player, world.multiworld, hint_text)
else:
regions[region_name] = Region(region_name, world.player, world.multiworld)
set_er_region_rules(world, world.ability_unlocks, regions, portal_pairs)
er_hint_data: Dict[int, str] = {}
for location_name, location_id in world.location_name_to_id.items():
region = regions[location_table[location_name].er_region]
location = TunicERLocation(world.player, location_name, location_id, region)
region.locations.append(location)
if region.name == region.hint_text:
continue
er_hint_data[location.address] = region.hint_text
create_randomized_entrances(portal_pairs, regions)
for region in regions.values():
world.multiworld.regions.append(region)
place_event_items(world, regions)
victory_region = regions["Spirit Arena Victory"]
victory_location = TunicERLocation(world.player, "The Heir", None, victory_region)
victory_location.place_locked_item(TunicERItem("Victory", ItemClassification.progression, None, world.player))
world.multiworld.completion_condition[world.player] = lambda state: state.has("Victory", world.player)
victory_region.locations.append(victory_location)
portals_and_hints = (portal_pairs, er_hint_data)
return portals_and_hints
tunic_events: Dict[str, str] = {
"Eastern Bell": "Forest Belltower Upper",
"Western Bell": "Overworld Belltower",
"Furnace Fuse": "Furnace Fuse",
"South and West Fortress Exterior Fuses": "Fortress Exterior from Overworld",
"Upper and Central Fortress Exterior Fuses": "Fortress Courtyard Upper",
"Beneath the Vault Fuse": "Beneath the Vault Back",
"Eastern Vault West Fuses": "Eastern Vault Fortress",
"Eastern Vault East Fuse": "Eastern Vault Fortress",
"Quarry Connector Fuse": "Quarry Connector",
"Quarry Fuse": "Quarry",
"Ziggurat Fuse": "Rooted Ziggurat Lower Back",
"West Garden Fuse": "West Garden",
"Library Fuse": "Library Lab",
}
def place_event_items(world: "TunicWorld", regions: Dict[str, Region]) -> None:
for event_name, region_name in tunic_events.items():
region = regions[region_name]
location = TunicERLocation(world.player, event_name, None, region)
if event_name.endswith("Bell"):
location.place_locked_item(
TunicERItem("Ring " + event_name, ItemClassification.progression, None, world.player))
else:
location.place_locked_item(
TunicERItem("Activate " + event_name, ItemClassification.progression, None, world.player))
region.locations.append(location)
# pairing off portals, starting with dead ends
def pair_portals(world: "TunicWorld") -> Dict[Portal, Portal]:
# separate the portals into dead ends and non-dead ends
portal_pairs: Dict[Portal, Portal] = {}
dead_ends: List[Portal] = []
two_plus: List[Portal] = []
fixed_shop = False
logic_rules = world.options.logic_rules.value
# create separate lists for dead ends and non-dead ends
if logic_rules:
for portal in portal_mapping:
if tunic_er_regions[portal.region].dead_end == 1:
dead_ends.append(portal)
else:
two_plus.append(portal)
else:
for portal in portal_mapping:
if tunic_er_regions[portal.region].dead_end:
dead_ends.append(portal)
else:
two_plus.append(portal)
connected_regions: Set[str] = set()
# make better start region stuff when/if implementing random start
start_region = "Overworld"
connected_regions.update(add_dependent_regions(start_region, logic_rules))
# need to plando fairy cave, or it could end up laurels locked
# fix this later to be random? probably not?
if world.options.laurels_location == "10_fairies":
portal1 = None
portal2 = None
for portal in two_plus:
if portal.scene_destination() == "Overworld Redux, Waterfall_":
portal1 = portal
break
for portal in dead_ends:
if portal.scene_destination() == "Waterfall, Overworld Redux_":
portal2 = portal
break
portal_pairs[portal1] = portal2
two_plus.remove(portal1)
dead_ends.remove(portal2)
if world.options.fixed_shop:
fixed_shop = True
portal1 = None
for portal in two_plus:
if portal.scene_destination() == "Overworld Redux, Windmill_":
portal1 = portal
break
portal2 = Portal(name="Shop Portal", region=f"Shop Entrance 2", destination="Previous Region_")
portal_pairs[portal1] = portal2
two_plus.remove(portal1)
# we want to start by making sure every region is accessible
non_dead_end_regions = set()
for region_name, region_info in tunic_er_regions.items():
if not region_info.dead_end:
non_dead_end_regions.add(region_name)
elif region_info.dead_end == 2 and logic_rules:
non_dead_end_regions.add(region_name)
world.random.shuffle(two_plus)
check_success = 0
portal1 = None
portal2 = None
while len(connected_regions) < len(non_dead_end_regions):
# find a portal in an inaccessible region
if check_success == 0:
for portal in two_plus:
if portal.region in connected_regions:
# if there's risk of self-locking, start over
if gate_before_switch(portal, two_plus):
world.random.shuffle(two_plus)
break
portal1 = portal
two_plus.remove(portal)
check_success = 1
break
# then we find a portal in a connected region
if check_success == 1:
for portal in two_plus:
if portal.region not in connected_regions:
# if there's risk of self-locking, shuffle and try again
if gate_before_switch(portal, two_plus):
world.random.shuffle(two_plus)
break
portal2 = portal
two_plus.remove(portal)
check_success = 2
break
# once we have both portals, connect them and add the new region(s) to connected_regions
if check_success == 2:
connected_regions.update(add_dependent_regions(portal2.region, logic_rules))
portal_pairs[portal1] = portal2
check_success = 0
world.random.shuffle(two_plus)
# add 6 shops, connect them to unique scenes
# this is due to a limitation in Tunic -- you wrong warp if there's multiple shops
shop_scenes: Set[str] = set()
shop_count = 6
if fixed_shop:
shop_count = 1
shop_scenes.add("Overworld Redux")
for i in range(shop_count):
portal1 = None
for portal in two_plus:
if portal.scene() not in shop_scenes:
shop_scenes.add(portal.scene())
portal1 = portal
two_plus.remove(portal)
break
if portal1 is None:
raise Exception("Too many shops in the pool, or something else went wrong")
portal2 = Portal(name="Shop Portal", region=f"Shop Entrance {i + 1}", destination="Previous Region_")
portal_pairs[portal1] = portal2
# connect dead ends to random non-dead ends
# none of the key events are in dead ends, so we don't need to do gate_before_switch
while len(dead_ends) > 0:
portal1 = two_plus.pop()
portal2 = dead_ends.pop()
portal_pairs[portal1] = portal2
# then randomly connect the remaining portals to each other
# every region is accessible, so gate_before_switch is not necessary
while len(two_plus) > 1:
portal1 = two_plus.pop()
portal2 = two_plus.pop()
portal_pairs[portal1] = portal2
if len(two_plus) == 1:
raise Exception("two plus had an odd number of portals, investigate this")
for portal1, portal2 in portal_pairs.items():
world.multiworld.spoiler.set_entrance(portal1.name, portal2.name, "both", world.player)
return portal_pairs
# loop through our list of paired portals and make two-way connections
def create_randomized_entrances(portal_pairs: Dict[Portal, Portal], regions: Dict[str, Region]) -> None:
for portal1, portal2 in portal_pairs.items():
region1 = regions[portal1.region]
region2 = regions[portal2.region]
region1.connect(region2, f"{portal1.name} -> {portal2.name}")
# prevent the logic from thinking you can get to any shop-connected region from the shop
if portal2.name != "Shop":
region2.connect(region1, f"{portal2.name} -> {portal1.name}")
# loop through the static connections, return regions you can reach from this region
def add_dependent_regions(region_name: str, logic_rules: int) -> Set[str]:
region_set = set()
if not logic_rules:
regions_to_add = dependent_regions
elif logic_rules == 1:
regions_to_add = dependent_regions_nmg
else:
regions_to_add = dependent_regions_ur
for origin_regions, destination_regions in regions_to_add.items():
if region_name in origin_regions:
# if you matched something in the first set, you get the regions in its paired set
region_set.update(destination_regions)
return region_set
# if you didn't match anything in the first sets, just gives you the region
region_set = {region_name}
return region_set
# we're checking if an event-locked portal is being placed before the regions where its key(s) is/are
# doing this ensures the keys will not be locked behind the event-locked portal
def gate_before_switch(check_portal: Portal, two_plus: List[Portal]) -> bool:
# the western belltower cannot be locked since you can access it with laurels
# so we only need to make sure the forest belltower isn't locked
if check_portal.scene_destination() == "Overworld Redux, Temple_main":
i = 0
for portal in two_plus:
if portal.region == "Forest Belltower Upper":
i += 1
break
if i == 1:
return True
# fortress big gold door needs 2 scenes and one of the two upper portals of the courtyard
elif check_portal.scene_destination() == "Fortress Main, Fortress Arena_":
i = j = k = 0
for portal in two_plus:
if portal.region == "Fortress Courtyard Upper":
i += 1
if portal.scene() == "Fortress Basement":
j += 1
if portal.region == "Eastern Vault Fortress":
k += 1
if i == 2 or j == 2 or k == 5:
return True
# fortress teleporter needs only the left fuses
elif check_portal.scene_destination() in ["Fortress Arena, Transit_teleporter_spidertank",
"Transit, Fortress Arena_teleporter_spidertank"]:
i = j = k = 0
for portal in two_plus:
if portal.scene() == "Fortress Courtyard":
i += 1
if portal.scene() == "Fortress Basement":
j += 1
if portal.region == "Eastern Vault Fortress":
k += 1
if i == 8 or j == 2 or k == 5:
return True
# Cathedral door needs Overworld and the front of Swamp
# Overworld is currently guaranteed, so no need to check it
elif check_portal.scene_destination() == "Swamp Redux 2, Cathedral Redux_main":
i = 0
for portal in two_plus:
if portal.region == "Swamp":
i += 1
if i == 4:
return True
# Zig portal room exit needs Zig 3 to be accessible to hit the fuse
elif check_portal.scene_destination() == "ziggurat2020_FTRoom, ziggurat2020_3_":
i = 0
for portal in two_plus:
if portal.scene() == "ziggurat2020_3":
i += 1
if i == 2:
return True
# Quarry teleporter needs you to hit the Darkwoods fuse
# Since it's physically in Quarry, we don't need to check for it
elif check_portal.scene_destination() in ["Quarry Redux, Transit_teleporter_quarry teleporter",
"Quarry Redux, ziggurat2020_0_"]:
i = 0
for portal in two_plus:
if portal.scene() == "Darkwoods Tunnel":
i += 1
if i == 2:
return True
# Same as above, but Quarry isn't guaranteed here
elif check_portal.scene_destination() == "Transit, Quarry Redux_teleporter_quarry teleporter":
i = j = 0
for portal in two_plus:
if portal.scene() == "Darkwoods Tunnel":
i += 1
if portal.scene() == "Quarry Redux":
j += 1
if i == 2 or j == 7:
return True
# Need Library fuse to use this teleporter
elif check_portal.scene_destination() == "Transit, Library Lab_teleporter_library teleporter":
i = 0
for portal in two_plus:
if portal.scene() == "Library Lab":
i += 1
if i == 3:
return True
# Need West Garden fuse to use this teleporter
elif check_portal.scene_destination() == "Transit, Archipelagos Redux_teleporter_archipelagos_teleporter":
i = 0
for portal in two_plus:
if portal.scene() == "Archipelagos Redux":
i += 1
if i == 6:
return True
# false means you're good to place the portal
return False

214
worlds/tunic/items.py Normal file
View File

@@ -0,0 +1,214 @@
from itertools import groupby
from typing import Dict, List, Set, NamedTuple
from BaseClasses import ItemClassification
class TunicItemData(NamedTuple):
classification: ItemClassification
quantity_in_item_pool: int
item_id_offset: int
item_group: str = ""
item_base_id = 509342400
item_table: Dict[str, TunicItemData] = {
"Firecracker x2": TunicItemData(ItemClassification.filler, 3, 0, "bombs"),
"Firecracker x3": TunicItemData(ItemClassification.filler, 3, 1, "bombs"),
"Firecracker x4": TunicItemData(ItemClassification.filler, 3, 2, "bombs"),
"Firecracker x5": TunicItemData(ItemClassification.filler, 1, 3, "bombs"),
"Firecracker x6": TunicItemData(ItemClassification.filler, 2, 4, "bombs"),
"Fire Bomb x2": TunicItemData(ItemClassification.filler, 2, 5, "bombs"),
"Fire Bomb x3": TunicItemData(ItemClassification.filler, 1, 6, "bombs"),
"Ice Bomb x2": TunicItemData(ItemClassification.filler, 2, 7, "bombs"),
"Ice Bomb x3": TunicItemData(ItemClassification.filler, 2, 8, "bombs"),
"Ice Bomb x5": TunicItemData(ItemClassification.filler, 1, 9, "bombs"),
"Lure": TunicItemData(ItemClassification.filler, 4, 10, "consumables"),
"Lure x2": TunicItemData(ItemClassification.filler, 1, 11, "consumables"),
"Pepper x2": TunicItemData(ItemClassification.filler, 4, 12, "consumables"),
"Ivy x3": TunicItemData(ItemClassification.filler, 2, 13, "consumables"),
"Effigy": TunicItemData(ItemClassification.useful, 12, 14, "money"),
"HP Berry": TunicItemData(ItemClassification.filler, 2, 15, "consumables"),
"HP Berry x2": TunicItemData(ItemClassification.filler, 4, 16, "consumables"),
"HP Berry x3": TunicItemData(ItemClassification.filler, 2, 17, "consumables"),
"MP Berry": TunicItemData(ItemClassification.filler, 4, 18, "consumables"),
"MP Berry x2": TunicItemData(ItemClassification.filler, 2, 19, "consumables"),
"MP Berry x3": TunicItemData(ItemClassification.filler, 7, 20, "consumables"),
"Fairy": TunicItemData(ItemClassification.progression, 20, 21),
"Stick": TunicItemData(ItemClassification.progression, 1, 22, "weapons"),
"Sword": TunicItemData(ItemClassification.progression, 3, 23, "weapons"),
"Sword Upgrade": TunicItemData(ItemClassification.progression, 4, 24, "weapons"),
"Magic Wand": TunicItemData(ItemClassification.progression, 1, 25, "weapons"),
"Magic Dagger": TunicItemData(ItemClassification.progression, 1, 26),
"Magic Orb": TunicItemData(ItemClassification.progression, 1, 27),
"Hero's Laurels": TunicItemData(ItemClassification.progression, 1, 28),
"Lantern": TunicItemData(ItemClassification.progression, 1, 29),
"Gun": TunicItemData(ItemClassification.useful, 1, 30, "weapons"),
"Shield": TunicItemData(ItemClassification.useful, 1, 31),
"Dath Stone": TunicItemData(ItemClassification.useful, 1, 32),
"Hourglass": TunicItemData(ItemClassification.useful, 1, 33),
"Old House Key": TunicItemData(ItemClassification.progression, 1, 34, "keys"),
"Key": TunicItemData(ItemClassification.progression, 2, 35, "keys"),
"Fortress Vault Key": TunicItemData(ItemClassification.progression, 1, 36, "keys"),
"Flask Shard": TunicItemData(ItemClassification.useful, 12, 37, "potions"),
"Potion Flask": TunicItemData(ItemClassification.useful, 5, 38, "potions"),
"Golden Coin": TunicItemData(ItemClassification.progression, 17, 39),
"Card Slot": TunicItemData(ItemClassification.useful, 4, 40),
"Red Questagon": TunicItemData(ItemClassification.progression_skip_balancing, 1, 41, "hexagons"),
"Green Questagon": TunicItemData(ItemClassification.progression_skip_balancing, 1, 42, "hexagons"),
"Blue Questagon": TunicItemData(ItemClassification.progression_skip_balancing, 1, 43, "hexagons"),
"Gold Questagon": TunicItemData(ItemClassification.progression_skip_balancing, 0, 44, "hexagons"),
"ATT Offering": TunicItemData(ItemClassification.useful, 4, 45, "offerings"),
"DEF Offering": TunicItemData(ItemClassification.useful, 4, 46, "offerings"),
"Potion Offering": TunicItemData(ItemClassification.useful, 3, 47, "offerings"),
"HP Offering": TunicItemData(ItemClassification.useful, 6, 48, "offerings"),
"MP Offering": TunicItemData(ItemClassification.useful, 3, 49, "offerings"),
"SP Offering": TunicItemData(ItemClassification.useful, 2, 50, "offerings"),
"Hero Relic - ATT": TunicItemData(ItemClassification.useful, 1, 51, "hero relics"),
"Hero Relic - DEF": TunicItemData(ItemClassification.useful, 1, 52, "hero relics"),
"Hero Relic - HP": TunicItemData(ItemClassification.useful, 1, 53, "hero relics"),
"Hero Relic - MP": TunicItemData(ItemClassification.useful, 1, 54, "hero relics"),
"Hero Relic - POTION": TunicItemData(ItemClassification.useful, 1, 55, "hero relics"),
"Hero Relic - SP": TunicItemData(ItemClassification.useful, 1, 56, "hero relics"),
"Orange Peril Ring": TunicItemData(ItemClassification.useful, 1, 57, "cards"),
"Tincture": TunicItemData(ItemClassification.useful, 1, 58, "cards"),
"Scavenger Mask": TunicItemData(ItemClassification.progression, 1, 59, "cards"),
"Cyan Peril Ring": TunicItemData(ItemClassification.useful, 1, 60, "cards"),
"Bracer": TunicItemData(ItemClassification.useful, 1, 61, "cards"),
"Dagger Strap": TunicItemData(ItemClassification.useful, 1, 62, "cards"),
"Inverted Ash": TunicItemData(ItemClassification.useful, 1, 63, "cards"),
"Lucky Cup": TunicItemData(ItemClassification.useful, 1, 64, "cards"),
"Magic Echo": TunicItemData(ItemClassification.useful, 1, 65, "cards"),
"Anklet": TunicItemData(ItemClassification.useful, 1, 66, "cards"),
"Muffling Bell": TunicItemData(ItemClassification.useful, 1, 67, "cards"),
"Glass Cannon": TunicItemData(ItemClassification.useful, 1, 68, "cards"),
"Perfume": TunicItemData(ItemClassification.useful, 1, 69, "cards"),
"Louder Echo": TunicItemData(ItemClassification.useful, 1, 70, "cards"),
"Aura's Gem": TunicItemData(ItemClassification.useful, 1, 71, "cards"),
"Bone Card": TunicItemData(ItemClassification.useful, 1, 72, "cards"),
"Mr Mayor": TunicItemData(ItemClassification.useful, 1, 73, "golden treasures"),
"Secret Legend": TunicItemData(ItemClassification.useful, 1, 74, "golden treasures"),
"Sacred Geometry": TunicItemData(ItemClassification.useful, 1, 75, "golden treasures"),
"Vintage": TunicItemData(ItemClassification.useful, 1, 76, "golden treasures"),
"Just Some Pals": TunicItemData(ItemClassification.useful, 1, 77, "golden treasures"),
"Regal Weasel": TunicItemData(ItemClassification.useful, 1, 78, "golden treasures"),
"Spring Falls": TunicItemData(ItemClassification.useful, 1, 79, "golden treasures"),
"Power Up": TunicItemData(ItemClassification.useful, 1, 80, "golden treasures"),
"Back To Work": TunicItemData(ItemClassification.useful, 1, 81, "golden treasures"),
"Phonomath": TunicItemData(ItemClassification.useful, 1, 82, "golden treasures"),
"Dusty": TunicItemData(ItemClassification.useful, 1, 83, "golden treasures"),
"Forever Friend": TunicItemData(ItemClassification.useful, 1, 84, "golden treasures"),
"Fool Trap": TunicItemData(ItemClassification.trap, 0, 85, "fool"),
"Money x1": TunicItemData(ItemClassification.filler, 3, 86, "money"),
"Money x10": TunicItemData(ItemClassification.filler, 1, 87, "money"),
"Money x15": TunicItemData(ItemClassification.filler, 10, 88, "money"),
"Money x16": TunicItemData(ItemClassification.filler, 1, 89, "money"),
"Money x20": TunicItemData(ItemClassification.filler, 17, 90, "money"),
"Money x25": TunicItemData(ItemClassification.filler, 14, 91, "money"),
"Money x30": TunicItemData(ItemClassification.filler, 4, 92, "money"),
"Money x32": TunicItemData(ItemClassification.filler, 4, 93, "money"),
"Money x40": TunicItemData(ItemClassification.filler, 3, 94, "money"),
"Money x48": TunicItemData(ItemClassification.filler, 1, 95, "money"),
"Money x50": TunicItemData(ItemClassification.filler, 7, 96, "money"),
"Money x64": TunicItemData(ItemClassification.filler, 1, 97, "money"),
"Money x100": TunicItemData(ItemClassification.filler, 5, 98, "money"),
"Money x128": TunicItemData(ItemClassification.useful, 3, 99, "money"),
"Money x200": TunicItemData(ItemClassification.useful, 1, 100, "money"),
"Money x255": TunicItemData(ItemClassification.useful, 1, 101, "money"),
"Pages 0-1": TunicItemData(ItemClassification.useful, 1, 102, "pages"),
"Pages 2-3": TunicItemData(ItemClassification.useful, 1, 103, "pages"),
"Pages 4-5": TunicItemData(ItemClassification.useful, 1, 104, "pages"),
"Pages 6-7": TunicItemData(ItemClassification.useful, 1, 105, "pages"),
"Pages 8-9": TunicItemData(ItemClassification.useful, 1, 106, "pages"),
"Pages 10-11": TunicItemData(ItemClassification.useful, 1, 107, "pages"),
"Pages 12-13": TunicItemData(ItemClassification.useful, 1, 108, "pages"),
"Pages 14-15": TunicItemData(ItemClassification.useful, 1, 109, "pages"),
"Pages 16-17": TunicItemData(ItemClassification.useful, 1, 110, "pages"),
"Pages 18-19": TunicItemData(ItemClassification.useful, 1, 111, "pages"),
"Pages 20-21": TunicItemData(ItemClassification.useful, 1, 112, "pages"),
"Pages 22-23": TunicItemData(ItemClassification.useful, 1, 113, "pages"),
"Pages 24-25 (Prayer)": TunicItemData(ItemClassification.progression, 1, 114, "pages"),
"Pages 26-27": TunicItemData(ItemClassification.useful, 1, 115, "pages"),
"Pages 28-29": TunicItemData(ItemClassification.useful, 1, 116, "pages"),
"Pages 30-31": TunicItemData(ItemClassification.useful, 1, 117, "pages"),
"Pages 32-33": TunicItemData(ItemClassification.useful, 1, 118, "pages"),
"Pages 34-35": TunicItemData(ItemClassification.useful, 1, 119, "pages"),
"Pages 36-37": TunicItemData(ItemClassification.useful, 1, 120, "pages"),
"Pages 38-39": TunicItemData(ItemClassification.useful, 1, 121, "pages"),
"Pages 40-41": TunicItemData(ItemClassification.useful, 1, 122, "pages"),
"Pages 42-43 (Holy Cross)": TunicItemData(ItemClassification.progression, 1, 123, "pages"),
"Pages 44-45": TunicItemData(ItemClassification.useful, 1, 124, "pages"),
"Pages 46-47": TunicItemData(ItemClassification.useful, 1, 125, "pages"),
"Pages 48-49": TunicItemData(ItemClassification.useful, 1, 126, "pages"),
"Pages 50-51": TunicItemData(ItemClassification.useful, 1, 127, "pages"),
"Pages 52-53 (Ice Rod)": TunicItemData(ItemClassification.progression, 1, 128, "pages"),
"Pages 54-55": TunicItemData(ItemClassification.useful, 1, 129, "pages"),
}
fool_tiers: List[List[str]] = [
[],
["Money x1", "Money x10", "Money x15", "Money x16"],
["Money x1", "Money x10", "Money x15", "Money x16", "Money x20"],
["Money x1", "Money x10", "Money x15", "Money x16", "Money x20", "Money x25", "Money x30"],
]
slot_data_item_names = [
"Stick",
"Sword",
"Sword Upgrade",
"Magic Dagger",
"Magic Wand",
"Magic Orb",
"Hero's Laurels",
"Lantern",
"Gun",
"Scavenger Mask",
"Shield",
"Dath Stone",
"Hourglass",
"Old House Key",
"Fortress Vault Key",
"Hero Relic - ATT",
"Hero Relic - DEF",
"Hero Relic - POTION",
"Hero Relic - HP",
"Hero Relic - SP",
"Hero Relic - MP",
"Pages 24-25 (Prayer)",
"Pages 42-43 (Holy Cross)",
"Pages 52-53 (Ice Rod)",
"Red Questagon",
"Green Questagon",
"Blue Questagon",
"Gold Questagon",
]
item_name_to_id: Dict[str, int] = {name: item_base_id + data.item_id_offset for name, data in item_table.items()}
filler_items: List[str] = [name for name, data in item_table.items() if data.classification == ItemClassification.filler]
def get_item_group(item_name: str) -> str:
return item_table[item_name].item_group
item_name_groups: Dict[str, Set[str]] = {
group: set(item_names) for group, item_names in groupby(sorted(item_table, key=get_item_group), get_item_group) if group != ""
}
# extra groups for the purpose of aliasing items
extra_groups: Dict[str, Set[str]] = {
"laurels": {"Hero's Laurels"},
"orb": {"Magic Orb"},
"dagger": {"Magic Dagger"},
"magic rod": {"Magic Wand"},
"holy cross": {"Pages 42-43 (Holy Cross)"},
"prayer": {"Pages 24-25 (Prayer)"},
"ice rod": {"Pages 52-53 (Ice Rod)"},
"melee weapons": {"Stick", "Sword", "Sword Upgrade"},
"progressive sword": {"Sword Upgrade"},
"abilities": {"Pages 24-25 (Prayer)", "Pages 42-43 (Holy Cross)", "Pages 52-53 (Ice Rod)"},
"questagons": {"Red Questagon", "Green Questagon", "Blue Questagon", "Gold Questagon"}
}
item_name_groups.update(extra_groups)

337
worlds/tunic/locations.py Normal file
View File

@@ -0,0 +1,337 @@
from typing import Dict, NamedTuple, Set
from itertools import groupby
class TunicLocationData(NamedTuple):
region: str
er_region: str # entrance rando region
location_group: str = "region"
location_base_id = 509342400
location_table: Dict[str, TunicLocationData] = {
"Beneath the Well - [Powered Secret Room] Chest": TunicLocationData("Beneath the Well", "Beneath the Well Back"),
"Beneath the Well - [Entryway] Chest": TunicLocationData("Beneath the Well", "Beneath the Well Main"),
"Beneath the Well - [Third Room] Beneath Platform Chest": TunicLocationData("Beneath the Well", "Beneath the Well Main"),
"Beneath the Well - [Third Room] Tentacle Chest": TunicLocationData("Beneath the Well", "Beneath the Well Main"),
"Beneath the Well - [Entryway] Obscured Behind Waterfall": TunicLocationData("Beneath the Well", "Beneath the Well Main"),
"Beneath the Well - [Save Room] Upper Floor Chest 1": TunicLocationData("Beneath the Well", "Beneath the Well Back"),
"Beneath the Well - [Save Room] Upper Floor Chest 2": TunicLocationData("Beneath the Well", "Beneath the Well Back"),
"Beneath the Well - [Second Room] Underwater Chest": TunicLocationData("Beneath the Well", "Beneath the Well Main"),
"Beneath the Well - [Back Corridor] Right Secret": TunicLocationData("Beneath the Well", "Beneath the Well Main"),
"Beneath the Well - [Back Corridor] Left Secret": TunicLocationData("Beneath the Well", "Beneath the Well Main"),
"Beneath the Well - [Second Room] Obscured Behind Waterfall": TunicLocationData("Beneath the Well", "Beneath the Well Main"),
"Beneath the Well - [Side Room] Chest By Pots": TunicLocationData("Beneath the Well", "Beneath the Well Main"),
"Beneath the Well - [Side Room] Chest By Phrends": TunicLocationData("Beneath the Well", "Beneath the Well Back"),
"Beneath the Well - [Second Room] Page": TunicLocationData("Beneath the Well", "Beneath the Well Main"),
"Dark Tomb Checkpoint - [Passage To Dark Tomb] Page Pickup": TunicLocationData("Beneath the Well", "Dark Tomb Checkpoint"),
"Cathedral - [1F] Guarded By Lasers": TunicLocationData("Cathedral", "Cathedral"),
"Cathedral - [1F] Near Spikes": TunicLocationData("Cathedral", "Cathedral"),
"Cathedral - [2F] Bird Room": TunicLocationData("Cathedral", "Cathedral"),
"Cathedral - [2F] Entryway Upper Walkway": TunicLocationData("Cathedral", "Cathedral"),
"Cathedral - [1F] Library": TunicLocationData("Cathedral", "Cathedral"),
"Cathedral - [2F] Library": TunicLocationData("Cathedral", "Cathedral"),
"Cathedral - [2F] Guarded By Lasers": TunicLocationData("Cathedral", "Cathedral"),
"Cathedral - [2F] Bird Room Secret": TunicLocationData("Cathedral", "Cathedral"),
"Cathedral - [1F] Library Secret": TunicLocationData("Cathedral", "Cathedral"),
"Dark Tomb - Spike Maze Near Exit": TunicLocationData("Dark Tomb", "Dark Tomb Main"),
"Dark Tomb - 2nd Laser Room": TunicLocationData("Dark Tomb", "Dark Tomb Main"),
"Dark Tomb - 1st Laser Room": TunicLocationData("Dark Tomb", "Dark Tomb Main"),
"Dark Tomb - Spike Maze Upper Walkway": TunicLocationData("Dark Tomb", "Dark Tomb Main"),
"Dark Tomb - Skulls Chest": TunicLocationData("Dark Tomb", "Dark Tomb Main"),
"Dark Tomb - Spike Maze Near Stairs": TunicLocationData("Dark Tomb", "Dark Tomb Main"),
"Dark Tomb - 1st Laser Room Obscured": TunicLocationData("Dark Tomb", "Dark Tomb Main"),
"Guardhouse 2 - Upper Floor": TunicLocationData("East Forest", "Guard House 2"),
"Guardhouse 2 - Bottom Floor Secret": TunicLocationData("East Forest", "Guard House 2"),
"Guardhouse 1 - Upper Floor Obscured": TunicLocationData("East Forest", "Guard House 1 East"),
"Guardhouse 1 - Upper Floor": TunicLocationData("East Forest", "Guard House 1 East"),
"East Forest - Dancing Fox Spirit Holy Cross": TunicLocationData("East Forest", "East Forest Dance Fox Spot", "holy cross"),
"East Forest - Golden Obelisk Holy Cross": TunicLocationData("East Forest", "East Forest", "holy cross"),
"East Forest - Ice Rod Grapple Chest": TunicLocationData("East Forest", "East Forest"),
"East Forest - Above Save Point": TunicLocationData("East Forest", "East Forest"),
"East Forest - Above Save Point Obscured": TunicLocationData("East Forest", "East Forest"),
"East Forest - From Guardhouse 1 Chest": TunicLocationData("East Forest", "East Forest Dance Fox Spot"),
"East Forest - Near Save Point": TunicLocationData("East Forest", "East Forest"),
"East Forest - Beneath Spider Chest": TunicLocationData("East Forest", "East Forest"),
"East Forest - Near Telescope": TunicLocationData("East Forest", "East Forest"),
"East Forest - Spider Chest": TunicLocationData("East Forest", "East Forest"),
"East Forest - Lower Dash Chest": TunicLocationData("East Forest", "East Forest"),
"East Forest - Lower Grapple Chest": TunicLocationData("East Forest", "East Forest"),
"East Forest - Bombable Wall": TunicLocationData("East Forest", "East Forest"),
"East Forest - Page On Teleporter": TunicLocationData("East Forest", "East Forest"),
"Forest Belltower - Near Save Point": TunicLocationData("East Forest", "Forest Belltower Lower"),
"Forest Belltower - After Guard Captain": TunicLocationData("East Forest", "Forest Belltower Upper"),
"Forest Belltower - Obscured Near Bell Top Floor": TunicLocationData("East Forest", "Forest Belltower Upper"),
"Forest Belltower - Obscured Beneath Bell Bottom Floor": TunicLocationData("East Forest", "Forest Belltower Main"),
"Forest Belltower - Page Pickup": TunicLocationData("East Forest", "Forest Belltower Main"),
"Forest Grave Path - Holy Cross Code by Grave": TunicLocationData("East Forest", "Forest Grave Path by Grave", "holy cross"),
"Forest Grave Path - Above Gate": TunicLocationData("East Forest", "Forest Grave Path Main"),
"Forest Grave Path - Obscured Chest": TunicLocationData("East Forest", "Forest Grave Path Main"),
"Forest Grave Path - Upper Walkway": TunicLocationData("East Forest", "Forest Grave Path Upper"),
"Forest Grave Path - Sword Pickup": TunicLocationData("East Forest", "Forest Grave Path by Grave"),
"Hero's Grave - Tooth Relic": TunicLocationData("East Forest", "Hero Relic - East Forest"),
"Fortress Courtyard - From East Belltower": TunicLocationData("East Forest", "Fortress Exterior from East Forest"),
"Fortress Leaf Piles - Secret Chest": TunicLocationData("Eastern Vault Fortress", "Fortress Leaf Piles"),
"Fortress Arena - Hexagon Red": TunicLocationData("Eastern Vault Fortress", "Fortress Arena"),
"Fortress Arena - Siege Engine/Vault Key Pickup": TunicLocationData("Eastern Vault Fortress", "Fortress Arena"),
"Fortress East Shortcut - Chest Near Slimes": TunicLocationData("Eastern Vault Fortress", "Fortress East Shortcut Lower"),
"Eastern Vault Fortress - [West Wing] Candles Holy Cross": TunicLocationData("Eastern Vault Fortress", "Eastern Vault Fortress", "holy cross"),
"Eastern Vault Fortress - [West Wing] Dark Room Chest 1": TunicLocationData("Eastern Vault Fortress", "Eastern Vault Fortress"),
"Eastern Vault Fortress - [West Wing] Dark Room Chest 2": TunicLocationData("Eastern Vault Fortress", "Eastern Vault Fortress"),
"Eastern Vault Fortress - [East Wing] Bombable Wall": TunicLocationData("Eastern Vault Fortress", "Eastern Vault Fortress"),
"Eastern Vault Fortress - [West Wing] Page Pickup": TunicLocationData("Eastern Vault Fortress", "Eastern Vault Fortress"),
"Fortress Grave Path - Upper Walkway": TunicLocationData("Eastern Vault Fortress", "Fortress Grave Path Upper"),
"Fortress Grave Path - Chest Right of Grave": TunicLocationData("Eastern Vault Fortress", "Fortress Grave Path"),
"Fortress Grave Path - Obscured Chest Left of Grave": TunicLocationData("Eastern Vault Fortress", "Fortress Grave Path"),
"Hero's Grave - Flowers Relic": TunicLocationData("Eastern Vault Fortress", "Hero Relic - Fortress"),
"Beneath the Fortress - Bridge": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"),
"Beneath the Fortress - Cell Chest 1": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"),
"Beneath the Fortress - Obscured Behind Waterfall": TunicLocationData("Beneath the Vault", "Beneath the Vault Front"),
"Beneath the Fortress - Back Room Chest": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"),
"Beneath the Fortress - Cell Chest 2": TunicLocationData("Beneath the Vault", "Beneath the Vault Back"),
"Frog's Domain - Near Vault": TunicLocationData("Frog's Domain", "Frog's Domain"),
"Frog's Domain - Slorm Room": TunicLocationData("Frog's Domain", "Frog's Domain"),
"Frog's Domain - Escape Chest": TunicLocationData("Frog's Domain", "Frog's Domain Back"),
"Frog's Domain - Grapple Above Hot Tub": TunicLocationData("Frog's Domain", "Frog's Domain"),
"Frog's Domain - Above Vault": TunicLocationData("Frog's Domain", "Frog's Domain"),
"Frog's Domain - Main Room Top Floor": TunicLocationData("Frog's Domain", "Frog's Domain"),
"Frog's Domain - Main Room Bottom Floor": TunicLocationData("Frog's Domain", "Frog's Domain"),
"Frog's Domain - Side Room Secret Passage": TunicLocationData("Frog's Domain", "Frog's Domain"),
"Frog's Domain - Side Room Chest": TunicLocationData("Frog's Domain", "Frog's Domain"),
"Frog's Domain - Side Room Grapple Secret": TunicLocationData("Frog's Domain", "Frog's Domain"),
"Frog's Domain - Magic Orb Pickup": TunicLocationData("Frog's Domain", "Frog's Domain"),
"Librarian - Hexagon Green": TunicLocationData("Library", "Library Arena"),
"Library Hall - Holy Cross Chest": TunicLocationData("Library", "Library Hall", "holy cross"),
"Library Lab - Chest By Shrine 2": TunicLocationData("Library", "Library Lab"),
"Library Lab - Chest By Shrine 1": TunicLocationData("Library", "Library Lab"),
"Library Lab - Chest By Shrine 3": TunicLocationData("Library", "Library Lab"),
"Library Lab - Behind Chalkboard by Fuse": TunicLocationData("Library", "Library Lab"),
"Library Lab - Page 3": TunicLocationData("Library", "Library Lab"),
"Library Lab - Page 1": TunicLocationData("Library", "Library Lab"),
"Library Lab - Page 2": TunicLocationData("Library", "Library Lab"),
"Hero's Grave - Mushroom Relic": TunicLocationData("Library", "Hero Relic - Library"),
"Lower Mountain - Page Before Door": TunicLocationData("Overworld", "Lower Mountain"),
"Changing Room - Normal Chest": TunicLocationData("Overworld", "Changing Room"),
"Fortress Courtyard - Chest Near Cave": TunicLocationData("Overworld", "Fortress Exterior near cave"),
"Fortress Courtyard - Near Fuse": TunicLocationData("Overworld", "Fortress Exterior from Overworld"),
"Fortress Courtyard - Below Walkway": TunicLocationData("Overworld", "Fortress Exterior from Overworld"),
"Fortress Courtyard - Page Near Cave": TunicLocationData("Overworld", "Fortress Exterior near cave"),
"West Furnace - Lantern Pickup": TunicLocationData("Overworld", "Furnace Fuse"),
"Maze Cave - Maze Room Chest": TunicLocationData("Overworld", "Maze Cave"),
"Old House - Normal Chest": TunicLocationData("Overworld", "Old House Front"),
"Old House - Shield Pickup": TunicLocationData("Overworld", "Old House Front"),
"Overworld - [West] Obscured Behind Windmill": TunicLocationData("Overworld", "Overworld"),
"Overworld - [South] Beach Chest": TunicLocationData("Overworld", "Overworld"),
"Overworld - [West] Obscured Near Well": TunicLocationData("Overworld", "Overworld"),
"Overworld - [Central] Bombable Wall": TunicLocationData("Overworld", "Overworld"),
"Overworld - [Northwest] Chest Near Turret": TunicLocationData("Overworld", "Overworld"),
"Overworld - [East] Chest Near Pots": TunicLocationData("Overworld", "Overworld"),
"Overworld - [Northwest] Chest Near Golden Obelisk": TunicLocationData("Overworld", "Overworld"),
"Overworld - [Southwest] South Chest Near Guard": TunicLocationData("Overworld", "Overworld"),
"Overworld - [Southwest] West Beach Guarded By Turret": TunicLocationData("Overworld", "Overworld"),
"Overworld - [Southwest] Chest Guarded By Turret": TunicLocationData("Overworld", "Overworld"),
"Overworld - [Northwest] Shadowy Corner Chest": TunicLocationData("Overworld", "Overworld"),
"Overworld - [Southwest] Obscured In Tunnel To Beach": TunicLocationData("Overworld", "Overworld"),
"Overworld - [Southwest] Grapple Chest Over Walkway": TunicLocationData("Overworld", "Overworld"),
"Overworld - [Northwest] Chest Beneath Quarry Gate": TunicLocationData("Overworld", "Overworld"),
"Overworld - [Southeast] Chest Near Swamp": TunicLocationData("Overworld", "Overworld"),
"Overworld - [Southwest] From West Garden": TunicLocationData("Overworld", "Overworld"),
"Overworld - [East] Grapple Chest": TunicLocationData("Overworld", "Overworld"),
"Overworld - [Southwest] West Beach Guarded By Turret 2": TunicLocationData("Overworld", "Overworld"),
"Overworld - [Southwest] Beach Chest Near Flowers": TunicLocationData("Overworld", "Overworld"),
"Overworld - [Southwest] Bombable Wall Near Fountain": TunicLocationData("Overworld", "Overworld"),
"Overworld - [West] Chest After Bell": TunicLocationData("Overworld", "Overworld Belltower"),
"Overworld - [Southwest] Tunnel Guarded By Turret": TunicLocationData("Overworld", "Overworld"),
"Overworld - [East] Between Ladders Near Ruined Passage": TunicLocationData("Overworld", "Overworld"),
"Overworld - [Northeast] Chest Above Patrol Cave": TunicLocationData("Overworld", "Overworld"),
"Overworld - [Southwest] Beach Chest Beneath Guard": TunicLocationData("Overworld", "Overworld"),
"Overworld - [Central] Chest Across From Well": TunicLocationData("Overworld", "Overworld"),
"Overworld - [Northwest] Chest Near Quarry Gate": TunicLocationData("Overworld", "Overworld"),
"Overworld - [East] Chest In Trees": TunicLocationData("Overworld", "Overworld"),
"Overworld - [West] Chest Behind Moss Wall": TunicLocationData("Overworld", "Overworld"),
"Overworld - [South] Beach Page": TunicLocationData("Overworld", "Overworld"),
"Overworld - [Southeast] Page on Pillar by Swamp": TunicLocationData("Overworld", "Overworld"),
"Overworld - [Southwest] Key Pickup": TunicLocationData("Overworld", "Overworld"),
"Overworld - [West] Key Pickup": TunicLocationData("Overworld", "Overworld"),
"Overworld - [East] Page Near Secret Shop": TunicLocationData("Overworld", "Overworld"),
"Overworld - [Southwest] Fountain Page": TunicLocationData("Overworld", "Overworld"),
"Overworld - [Northwest] Page on Pillar by Dark Tomb": TunicLocationData("Overworld", "Overworld"),
"Overworld - [Northwest] Fire Wand Pickup": TunicLocationData("Overworld", "Overworld"),
"Overworld - [West] Page On Teleporter": TunicLocationData("Overworld", "Overworld"),
"Overworld - [Northwest] Page By Well": TunicLocationData("Overworld", "Overworld"),
"Patrol Cave - Normal Chest": TunicLocationData("Overworld", "Patrol Cave"),
"Ruined Shop - Chest 1": TunicLocationData("Overworld", "Ruined Shop"),
"Ruined Shop - Chest 2": TunicLocationData("Overworld", "Ruined Shop"),
"Ruined Shop - Chest 3": TunicLocationData("Overworld", "Ruined Shop"),
"Ruined Passage - Page Pickup": TunicLocationData("Overworld", "Ruined Passage"),
"Shop - Potion 1": TunicLocationData("Overworld", "Shop", "shop"),
"Shop - Potion 2": TunicLocationData("Overworld", "Shop", "shop"),
"Shop - Coin 1": TunicLocationData("Overworld", "Shop", "shop"),
"Shop - Coin 2": TunicLocationData("Overworld", "Shop", "shop"),
"Special Shop - Secret Page Pickup": TunicLocationData("Overworld", "Special Shop"),
"Stick House - Stick Chest": TunicLocationData("Overworld", "Stick House"),
"Sealed Temple - Page Pickup": TunicLocationData("Overworld", "Sealed Temple"),
"Hourglass Cave - Hourglass Chest": TunicLocationData("Overworld", "Hourglass Cave"),
"Far Shore - Secret Chest": TunicLocationData("Overworld", "Far Shore"),
"Far Shore - Page Pickup": TunicLocationData("Overworld", "Far Shore to Spawn"),
"Coins in the Well - 10 Coins": TunicLocationData("Overworld", "Overworld", "well"),
"Coins in the Well - 15 Coins": TunicLocationData("Overworld", "Overworld", "well"),
"Coins in the Well - 3 Coins": TunicLocationData("Overworld", "Overworld", "well"),
"Coins in the Well - 6 Coins": TunicLocationData("Overworld", "Overworld", "well"),
"Secret Gathering Place - 20 Fairy Reward": TunicLocationData("Overworld", "Secret Gathering Place", "fairies"),
"Secret Gathering Place - 10 Fairy Reward": TunicLocationData("Overworld", "Secret Gathering Place", "fairies"),
"Overworld - [West] Moss Wall Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", "holy cross"),
"Overworld - [Southwest] Flowers Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", "holy cross"),
"Overworld - [Southwest] Fountain Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", "holy cross"),
"Overworld - [Northeast] Flowers Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", "holy cross"),
"Overworld - [East] Weathervane Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", "holy cross"),
"Overworld - [West] Windmill Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", "holy cross"),
"Overworld - [Southwest] Haiku Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", "holy cross"),
"Overworld - [West] Windchimes Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", "holy cross"),
"Overworld - [South] Starting Platform Holy Cross": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", "holy cross"),
"Overworld - [Northwest] Golden Obelisk Page": TunicLocationData("Overworld Holy Cross", "Overworld Holy Cross", "holy cross"),
"Old House - Holy Cross Door Page": TunicLocationData("Overworld Holy Cross", "Old House Back", "holy cross"),
"Cube Cave - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Cube Cave", "holy cross"),
"Southeast Cross Door - Chest 3": TunicLocationData("Overworld Holy Cross", "Southeast Cross Room", "holy cross"),
"Southeast Cross Door - Chest 2": TunicLocationData("Overworld Holy Cross", "Southeast Cross Room", "holy cross"),
"Southeast Cross Door - Chest 1": TunicLocationData("Overworld Holy Cross", "Southeast Cross Room", "holy cross"),
"Maze Cave - Maze Room Holy Cross": TunicLocationData("Overworld Holy Cross", "Maze Cave", "holy cross"),
"Caustic Light Cave - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Caustic Light Cave", "holy cross"),
"Old House - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Old House Front", "holy cross"),
"Patrol Cave - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Patrol Cave", "holy cross"),
"Ruined Passage - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Ruined Passage", "holy cross"),
"Hourglass Cave - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Hourglass Cave", "holy cross"),
"Sealed Temple - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Sealed Temple", "holy cross"),
"Fountain Cross Door - Page Pickup": TunicLocationData("Overworld Holy Cross", "Fountain Cross Room", "holy cross"),
"Secret Gathering Place - Holy Cross Chest": TunicLocationData("Overworld Holy Cross", "Secret Gathering Place", "holy cross"),
"Top of the Mountain - Page At The Peak": TunicLocationData("Overworld Holy Cross", "Top of the Mountain", "holy cross"),
"Monastery - Monastery Chest": TunicLocationData("Quarry", "Monastery Back"),
"Quarry - [Back Entrance] Bushes Holy Cross": TunicLocationData("Quarry Back", "Quarry Back", "holy cross"),
"Quarry - [Back Entrance] Chest": TunicLocationData("Quarry Back", "Quarry Back"),
"Quarry - [Central] Near Shortcut Ladder": TunicLocationData("Quarry", "Quarry"),
"Quarry - [East] Near Telescope": TunicLocationData("Quarry", "Quarry"),
"Quarry - [East] Upper Floor": TunicLocationData("Quarry", "Quarry"),
"Quarry - [Central] Below Entry Walkway": TunicLocationData("Quarry", "Quarry"),
"Quarry - [East] Obscured Near Winding Staircase": TunicLocationData("Quarry", "Quarry"),
"Quarry - [East] Obscured Beneath Scaffolding": TunicLocationData("Quarry", "Quarry"),
"Quarry - [East] Obscured Near Telescope": TunicLocationData("Quarry", "Quarry"),
"Quarry - [Back Entrance] Obscured Behind Wall": TunicLocationData("Quarry Back", "Quarry Back"),
"Quarry - [Central] Obscured Below Entry Walkway": TunicLocationData("Quarry", "Quarry"),
"Quarry - [Central] Top Floor Overhang": TunicLocationData("Quarry", "Quarry"),
"Quarry - [East] Near Bridge": TunicLocationData("Quarry", "Quarry"),
"Quarry - [Central] Above Ladder": TunicLocationData("Quarry", "Quarry Monastery Entry"),
"Quarry - [Central] Obscured Behind Staircase": TunicLocationData("Quarry", "Quarry"),
"Quarry - [Central] Above Ladder Dash Chest": TunicLocationData("Quarry", "Quarry Monastery Entry"),
"Quarry - [West] Upper Area Bombable Wall": TunicLocationData("Quarry Back", "Quarry Back"),
"Quarry - [East] Bombable Wall": TunicLocationData("Quarry", "Quarry"),
"Hero's Grave - Ash Relic": TunicLocationData("Quarry", "Hero Relic - Quarry"),
"Quarry - [West] Shooting Range Secret Path": TunicLocationData("Lower Quarry", "Lower Quarry"),
"Quarry - [West] Near Shooting Range": TunicLocationData("Lower Quarry", "Lower Quarry"),
"Quarry - [West] Below Shooting Range": TunicLocationData("Lower Quarry", "Lower Quarry"),
"Quarry - [Lowlands] Below Broken Ladder": TunicLocationData("Lower Quarry", "Lower Quarry"),
"Quarry - [West] Upper Area Near Waterfall": TunicLocationData("Lower Quarry", "Lower Quarry"),
"Quarry - [Lowlands] Upper Walkway": TunicLocationData("Lower Quarry", "Lower Quarry"),
"Quarry - [West] Lower Area Below Bridge": TunicLocationData("Lower Quarry", "Lower Quarry"),
"Quarry - [West] Lower Area Isolated Chest": TunicLocationData("Lower Quarry", "Lower Quarry"),
"Quarry - [Lowlands] Near Elevator": TunicLocationData("Lower Quarry", "Lower Quarry"),
"Quarry - [West] Lower Area After Bridge": TunicLocationData("Lower Quarry", "Lower Quarry"),
"Rooted Ziggurat Upper - Near Bridge Switch": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Upper Front"),
"Rooted Ziggurat Upper - Beneath Bridge To Administrator": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Upper Back"),
"Rooted Ziggurat Tower - Inside Tower": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Middle Top"),
"Rooted Ziggurat Lower - Near Corpses": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"),
"Rooted Ziggurat Lower - Spider Ambush": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"),
"Rooted Ziggurat Lower - Left Of Checkpoint Before Fuse": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"),
"Rooted Ziggurat Lower - After Guarded Fuse": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"),
"Rooted Ziggurat Lower - Guarded By Double Turrets": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"),
"Rooted Ziggurat Lower - After 2nd Double Turret Chest": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"),
"Rooted Ziggurat Lower - Guarded By Double Turrets 2": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Front"),
"Rooted Ziggurat Lower - Hexagon Blue": TunicLocationData("Rooted Ziggurat", "Rooted Ziggurat Lower Back"),
"Ruined Atoll - [West] Near Kevin Block": TunicLocationData("Ruined Atoll", "Ruined Atoll"),
"Ruined Atoll - [South] Upper Floor On Power Line": TunicLocationData("Ruined Atoll", "Ruined Atoll"),
"Ruined Atoll - [South] Chest Near Big Crabs": TunicLocationData("Ruined Atoll", "Ruined Atoll"),
"Ruined Atoll - [North] Guarded By Bird": TunicLocationData("Ruined Atoll", "Ruined Atoll"),
"Ruined Atoll - [Northeast] Chest Beneath Brick Walkway": TunicLocationData("Ruined Atoll", "Ruined Atoll"),
"Ruined Atoll - [Northwest] Bombable Wall": TunicLocationData("Ruined Atoll", "Ruined Atoll"),
"Ruined Atoll - [North] Obscured Beneath Bridge": TunicLocationData("Ruined Atoll", "Ruined Atoll"),
"Ruined Atoll - [South] Upper Floor On Bricks": TunicLocationData("Ruined Atoll", "Ruined Atoll"),
"Ruined Atoll - [South] Near Birds": TunicLocationData("Ruined Atoll", "Ruined Atoll"),
"Ruined Atoll - [Northwest] Behind Envoy": TunicLocationData("Ruined Atoll", "Ruined Atoll"),
"Ruined Atoll - [Southwest] Obscured Behind Fuse": TunicLocationData("Ruined Atoll", "Ruined Atoll"),
"Ruined Atoll - [East] Locked Room Upper Chest": TunicLocationData("Ruined Atoll", "Ruined Atoll"),
"Ruined Atoll - [North] From Lower Overworld Entrance": TunicLocationData("Ruined Atoll", "Ruined Atoll Lower Entry Area"),
"Ruined Atoll - [East] Locked Room Lower Chest": TunicLocationData("Ruined Atoll", "Ruined Atoll"),
"Ruined Atoll - [Northeast] Chest On Brick Walkway": TunicLocationData("Ruined Atoll", "Ruined Atoll"),
"Ruined Atoll - [Southeast] Chest Near Fuse": TunicLocationData("Ruined Atoll", "Ruined Atoll"),
"Ruined Atoll - [Northeast] Key Pickup": TunicLocationData("Ruined Atoll", "Ruined Atoll"),
"Cathedral Gauntlet - Gauntlet Reward": TunicLocationData("Swamp", "Cathedral Gauntlet"),
"Cathedral - Secret Legend Trophy Chest": TunicLocationData("Swamp", "Cathedral Secret Legend Room"),
"Swamp - [Upper Graveyard] Obscured Behind Hill": TunicLocationData("Swamp", "Swamp"),
"Swamp - [South Graveyard] 4 Orange Skulls": TunicLocationData("Swamp", "Swamp"),
"Swamp - [Central] Near Ramps Up": TunicLocationData("Swamp", "Swamp"),
"Swamp - [Upper Graveyard] Near Shield Fleemers": TunicLocationData("Swamp", "Swamp"),
"Swamp - [South Graveyard] Obscured Behind Ridge": TunicLocationData("Swamp", "Swamp"),
"Swamp - [South Graveyard] Obscured Beneath Telescope": TunicLocationData("Swamp", "Swamp"),
"Swamp - [Entrance] Above Entryway": TunicLocationData("Swamp", "Back of Swamp Laurels Area"),
"Swamp - [Central] South Secret Passage": TunicLocationData("Swamp", "Swamp"),
"Swamp - [South Graveyard] Upper Walkway On Pedestal": TunicLocationData("Swamp", "Swamp"),
"Swamp - [South Graveyard] Guarded By Tentacles": TunicLocationData("Swamp", "Swamp"),
"Swamp - [Upper Graveyard] Near Telescope": TunicLocationData("Swamp", "Swamp"),
"Swamp - [Outside Cathedral] Near Moonlight Bridge Door": TunicLocationData("Swamp", "Swamp"),
"Swamp - [Entrance] Obscured Inside Watchtower": TunicLocationData("Swamp", "Swamp"),
"Swamp - [Entrance] South Near Fence": TunicLocationData("Swamp", "Swamp"),
"Swamp - [South Graveyard] Guarded By Big Skeleton": TunicLocationData("Swamp", "Swamp"),
"Swamp - [South Graveyard] Chest Near Graves": TunicLocationData("Swamp", "Swamp"),
"Swamp - [Entrance] North Small Island": TunicLocationData("Swamp", "Swamp"),
"Swamp - [Outside Cathedral] Obscured Behind Memorial": TunicLocationData("Swamp", "Back of Swamp"),
"Swamp - [Central] Obscured Behind Northern Mountain": TunicLocationData("Swamp", "Swamp"),
"Swamp - [South Graveyard] Upper Walkway Dash Chest": TunicLocationData("Swamp", "Swamp"),
"Swamp - [South Graveyard] Above Big Skeleton": TunicLocationData("Swamp", "Swamp"),
"Swamp - [Central] Beneath Memorial": TunicLocationData("Swamp", "Swamp"),
"Hero's Grave - Feathers Relic": TunicLocationData("Swamp", "Hero Relic - Swamp"),
"West Furnace - Chest": TunicLocationData("West Garden", "Furnace Walking Path"),
"Overworld - [West] Near West Garden Entrance": TunicLocationData("West Garden", "Overworld to West Garden from Furnace"),
"West Garden - [Central Highlands] Holy Cross (Blue Lines)": TunicLocationData("West Garden", "West Garden", "holy cross"),
"West Garden - [West Lowlands] Tree Holy Cross Chest": TunicLocationData("West Garden", "West Garden", "holy cross"),
"West Garden - [Southeast Lowlands] Outside Cave": TunicLocationData("West Garden", "West Garden"),
"West Garden - [Central Lowlands] Chest Beneath Faeries": TunicLocationData("West Garden", "West Garden"),
"West Garden - [North] Behind Holy Cross Door": TunicLocationData("West Garden", "West Garden", "holy cross"),
"West Garden - [Central Highlands] Top of Ladder Before Boss": TunicLocationData("West Garden", "West Garden"),
"West Garden - [Central Lowlands] Passage Beneath Bridge": TunicLocationData("West Garden", "West Garden"),
"West Garden - [North] Across From Page Pickup": TunicLocationData("West Garden", "West Garden"),
"West Garden - [Central Lowlands] Below Left Walkway": TunicLocationData("West Garden", "West Garden"),
"West Garden - [West] In Flooded Walkway": TunicLocationData("West Garden", "West Garden"),
"West Garden - [West] Past Flooded Walkway": TunicLocationData("West Garden", "West Garden"),
"West Garden - [North] Obscured Beneath Hero's Memorial": TunicLocationData("West Garden", "West Garden"),
"West Garden - [Central Lowlands] Chest Near Shortcut Bridge": TunicLocationData("West Garden", "West Garden"),
"West Garden - [West Highlands] Upper Left Walkway": TunicLocationData("West Garden", "West Garden"),
"West Garden - [Central Lowlands] Chest Beneath Save Point": TunicLocationData("West Garden", "West Garden"),
"West Garden - [Central Highlands] Behind Guard Captain": TunicLocationData("West Garden", "West Garden"),
"West Garden - [Central Highlands] After Garden Knight": TunicLocationData("West Garden", "West Garden after Boss"),
"West Garden - [South Highlands] Secret Chest Beneath Fuse": TunicLocationData("West Garden", "West Garden"),
"West Garden - [East Lowlands] Page Behind Ice Dagger House": TunicLocationData("West Garden", "West Garden Portal Item"),
"West Garden - [North] Page Pickup": TunicLocationData("West Garden", "West Garden"),
"West Garden House - [Southeast Lowlands] Ice Dagger Pickup": TunicLocationData("West Garden", "Magic Dagger House"),
"Hero's Grave - Effigy Relic": TunicLocationData("West Garden", "Hero Relic - West Garden"),
}
hexagon_locations: Dict[str, str] = {
"Red Questagon": "Fortress Arena - Siege Engine/Vault Key Pickup",
"Green Questagon": "Librarian - Hexagon Green",
"Blue Questagon": "Rooted Ziggurat Lower - Hexagon Blue",
}
location_name_to_id: Dict[str, int] = {name: location_base_id + index for index, name in enumerate(location_table)}
def get_loc_group(location_name: str) -> str:
loc_group = location_table[location_name].location_group
if loc_group == "region":
# set loc_group as the region name. Typically, location groups are lowercase
loc_group = location_table[location_name].region.lower()
return loc_group
location_name_groups: Dict[str, Set[str]] = {
group: set(item_names) for group, item_names in groupby(sorted(location_table, key=get_loc_group), get_loc_group)
}

147
worlds/tunic/options.py Normal file
View File

@@ -0,0 +1,147 @@
from dataclasses import dataclass
from Options import DefaultOnToggle, Toggle, StartInventoryPool, Choice, Range, PerGameCommonOptions
class SwordProgression(DefaultOnToggle):
"""Adds four sword upgrades to the item pool that will progressively grant stronger melee weapons, including two new
swords with increased range and attack power."""
internal_name = "sword_progression"
display_name = "Sword Progression"
class StartWithSword(Toggle):
"""Start with a sword in the player's inventory. Does not count towards Sword Progression."""
internal_name = "start_with_sword"
display_name = "Start With Sword"
class KeysBehindBosses(Toggle):
"""Places the three hexagon keys behind their respective boss fight in your world."""
internal_name = "keys_behind_bosses"
display_name = "Keys Behind Bosses"
class AbilityShuffling(Toggle):
"""Locks the usage of Prayer, Holy Cross*, and Ice Rod until the relevant pages of the manual have been found.
If playing Hexagon Quest, abilities are instead randomly unlocked after obtaining 25%, 50%, and 75% of the required
Hexagon goal amount.
*Certain Holy Cross usages are still allowed, such as the free bomb codes, the seeking spell, and other
player-facing codes.
"""
internal_name = "ability_shuffling"
display_name = "Ability Shuffling"
class LogicRules(Choice):
"""Set which logic rules to use for your world.
Restricted: Standard logic, no glitches.
No Major Glitches: Ice grapples through doors, shooting the west bell, and boss quick kills are included in logic.
Unrestricted: Logic in No Major Glitches, as well as ladder storage to get to certain places early.
*Special Shop is not in logic without the Hero's Laurels in Unrestricted due to soft lock potential.
*Using Ladder Storage to get to individual chests is not in logic to avoid tedium.
*Getting knocked out of the air by enemies during Ladder Storage to reach places is not in logic, except for in
Rooted Ziggurat Lower. This is so you're not punished for playing with enemy rando on."""
internal_name = "logic_rules"
display_name = "Logic Rules"
option_restricted = 0
option_no_major_glitches = 1
option_unrestricted = 2
default = 0
class Lanternless(Toggle):
"""Choose whether you require the Lantern for dark areas.
When enabled, the Lantern is marked as Useful instead of Progression."""
internal_name = "lanternless"
display_name = "Lanternless"
class Maskless(Toggle):
"""Choose whether you require the Scavenger's Mask for Lower Quarry.
When enabled, the Scavenger's Mask is marked as Useful instead of Progression."""
internal_name = "maskless"
display_name = "Maskless"
class FoolTraps(Choice):
"""Replaces low-to-medium value money rewards in the item pool with fool traps, which cause random negative
effects to the player."""
internal_name = "fool_traps"
display_name = "Fool Traps"
option_off = 0
option_normal = 1
option_double = 2
option_onslaught = 3
default = 1
class HexagonQuest(Toggle):
"""An alternate goal that shuffles Gold "Questagon" items into the item pool and allows the game to be completed
after collecting the required number of them."""
internal_name = "hexagon_quest"
display_name = "Hexagon Quest"
class HexagonGoal(Range):
"""How many Gold Questagons are required to complete the game on Hexagon Quest."""
internal_name = "hexagon_goal"
display_name = "Gold Hexagons Required"
range_start = 15
range_end = 50
default = 20
class ExtraHexagonPercentage(Range):
"""How many extra Gold Questagons are shuffled into the item pool, taken as a percentage of the goal amount."""
internal_name = "extra_hexagon_percentage"
display_name = "Percentage of Extra Gold Hexagons"
range_start = 0
range_end = 100
default = 50
class EntranceRando(Toggle):
"""Randomize the connections between scenes.
A small, very lost fox on a big adventure."""
internal_name = "entrance_rando"
display_name = "Entrance Rando"
class FixedShop(Toggle):
"""Forces the Windmill entrance to lead to a shop, and places only one other shop in the pool.
Has no effect if Entrance Rando is not enabled."""
internal_name = "fixed_shop"
display_name = "ER Fixed Shop"
class LaurelsLocation(Choice):
"""Force the Hero's Laurels to be placed at a location in your world.
For if you want to avoid or specify early or late Laurels.
If you use the 10 Fairies option in Entrance Rando, Secret Gathering Place will be at its vanilla entrance."""
internal_name = "laurels_location"
display_name = "Laurels Location"
option_anywhere = 0
option_6_coins = 1
option_10_coins = 2
option_10_fairies = 3
default = 0
@dataclass
class TunicOptions(PerGameCommonOptions):
sword_progression: SwordProgression
start_with_sword: StartWithSword
keys_behind_bosses: KeysBehindBosses
ability_shuffling: AbilityShuffling
logic_rules: LogicRules
entrance_rando: EntranceRando
fixed_shop: FixedShop
fool_traps: FoolTraps
hexagon_quest: HexagonQuest
hexagon_goal: HexagonGoal
extra_hexagon_percentage: ExtraHexagonPercentage
lanternless: Lanternless
maskless: Maskless
laurels_location: LaurelsLocation
start_inventory_from_pool: StartInventoryPool

25
worlds/tunic/regions.py Normal file
View File

@@ -0,0 +1,25 @@
from typing import Dict, Set
tunic_regions: Dict[str, Set[str]] = {
"Menu": {"Overworld"},
"Overworld": {"Overworld Holy Cross", "East Forest", "Dark Tomb", "Beneath the Well", "West Garden",
"Ruined Atoll", "Eastern Vault Fortress", "Beneath the Vault", "Quarry Back", "Quarry", "Swamp",
"Spirit Arena"},
"Overworld Holy Cross": set(),
"East Forest": {"Eastern Vault Fortress"},
"Dark Tomb": {"West Garden"},
"Beneath the Well": {"Dark Tomb"},
"West Garden": {"Overworld", "Dark Tomb"},
"Ruined Atoll": {"Frog's Domain", "Library"},
"Frog's Domain": set(),
"Library": set(),
"Eastern Vault Fortress": {"Beneath the Vault"},
"Beneath the Vault": {"Eastern Vault Fortress"},
"Quarry Back": {"Quarry"},
"Quarry": {"Lower Quarry", "Rooted Ziggurat"},
"Lower Quarry": {"Rooted Ziggurat"},
"Rooted Ziggurat": set(),
"Swamp": {"Cathedral"},
"Cathedral": set(),
"Spirit Arena": set()
}

345
worlds/tunic/rules.py Normal file
View File

@@ -0,0 +1,345 @@
from random import Random
from typing import Dict, TYPE_CHECKING
from worlds.generic.Rules import set_rule, forbid_item
from BaseClasses import CollectionState
from .options import TunicOptions
if TYPE_CHECKING:
from . import TunicWorld
laurels = "Hero's Laurels"
grapple = "Magic Orb"
ice_dagger = "Magic Dagger"
fire_wand = "Magic Wand"
lantern = "Lantern"
fairies = "Fairy"
coins = "Golden Coin"
prayer = "Pages 24-25 (Prayer)"
holy_cross = "Pages 42-43 (Holy Cross)"
ice_rod = "Pages 52-53 (Ice Rod)"
key = "Key"
house_key = "Old House Key"
vault_key = "Fortress Vault Key"
mask = "Scavenger Mask"
red_hexagon = "Red Questagon"
green_hexagon = "Green Questagon"
blue_hexagon = "Blue Questagon"
gold_hexagon = "Gold Questagon"
def randomize_ability_unlocks(random: Random, options: TunicOptions) -> Dict[str, int]:
ability_requirement = [1, 1, 1]
if options.hexagon_quest.value:
hexagon_goal = options.hexagon_goal.value
# Set ability unlocks to 25, 50, and 75% of goal amount
ability_requirement = [hexagon_goal // 4, hexagon_goal // 2, hexagon_goal * 3 // 4]
abilities = [prayer, holy_cross, ice_rod]
random.shuffle(abilities)
return dict(zip(abilities, ability_requirement))
def has_ability(state: CollectionState, player: int, ability: str, options: TunicOptions,
ability_unlocks: Dict[str, int]) -> bool:
if not options.ability_shuffling:
return True
if options.hexagon_quest:
return state.has(gold_hexagon, player, ability_unlocks[ability])
return state.has(ability, player)
# a check to see if you can whack things in melee at all
def has_stick(state: CollectionState, player: int) -> bool:
return state.has("Stick", player) or state.has("Sword Upgrade", player, 1) or state.has("Sword", player)
def has_sword(state: CollectionState, player: int) -> bool:
return state.has("Sword", player) or state.has("Sword Upgrade", player, 2)
def has_ice_grapple_logic(long_range: bool, state: CollectionState, player: int, options: TunicOptions,
ability_unlocks: Dict[str, int]) -> bool:
if not options.logic_rules:
return False
if not long_range:
return state.has_all({ice_dagger, grapple}, player)
else:
return state.has_all({ice_dagger, fire_wand, grapple}, player) and \
has_ability(state, player, ice_rod, options, ability_unlocks)
def can_ladder_storage(state: CollectionState, player: int, options: TunicOptions) -> bool:
if options.logic_rules == "unrestricted" and has_stick(state, player):
return True
else:
return False
def has_mask(state: CollectionState, player: int, options: TunicOptions) -> bool:
if options.maskless:
return True
else:
return state.has(mask, player)
def has_lantern(state: CollectionState, player: int, options: TunicOptions) -> bool:
if options.lanternless:
return True
else:
return state.has(lantern, player)
def set_region_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) -> None:
multiworld = world.multiworld
player = world.player
options = world.options
multiworld.get_entrance("Overworld -> Overworld Holy Cross", player).access_rule = \
lambda state: has_ability(state, player, holy_cross, options, ability_unlocks)
multiworld.get_entrance("Overworld -> Beneath the Well", player).access_rule = \
lambda state: has_stick(state, player) or state.has(fire_wand, player)
multiworld.get_entrance("Overworld -> Dark Tomb", player).access_rule = \
lambda state: has_lantern(state, player, options)
multiworld.get_entrance("Overworld -> West Garden", player).access_rule = \
lambda state: state.has(laurels, player) \
or can_ladder_storage(state, player, options)
multiworld.get_entrance("Beneath the Well -> Dark Tomb", player).access_rule = \
lambda state: has_lantern(state, player, options)
multiworld.get_entrance("West Garden -> Dark Tomb", player).access_rule = \
lambda state: has_lantern(state, player, options)
multiworld.get_entrance("Overworld -> Eastern Vault Fortress", player).access_rule = \
lambda state: state.has(laurels, player) \
or has_ice_grapple_logic(True, state, player, options, ability_unlocks) \
or can_ladder_storage(state, player, options)
multiworld.get_entrance("East Forest -> Eastern Vault Fortress", player).access_rule = \
lambda state: state.has(laurels, player) \
or has_ice_grapple_logic(True, state, player, options, ability_unlocks) \
or can_ladder_storage(state, player, options)
# using laurels or ls to get in is covered by the -> Eastern Vault Fortress rules
multiworld.get_entrance("Overworld -> Beneath the Vault", player).access_rule = \
lambda state: has_lantern(state, player, options) and \
has_ability(state, player, prayer, options, ability_unlocks)
multiworld.get_entrance("Ruined Atoll -> Library", player).access_rule = \
lambda state: state.has_any({grapple, laurels}, player) and \
has_ability(state, player, prayer, options, ability_unlocks)
multiworld.get_entrance("Overworld -> Quarry", player).access_rule = \
lambda state: (has_sword(state, player) or state.has(fire_wand, player)) \
and (state.has_any({grapple, laurels}, player) or can_ladder_storage(state, player, options))
multiworld.get_entrance("Quarry Back -> Quarry", player).access_rule = \
lambda state: has_sword(state, player) or state.has(fire_wand, player)
multiworld.get_entrance("Quarry -> Lower Quarry", player).access_rule = \
lambda state: has_mask(state, player, options)
multiworld.get_entrance("Lower Quarry -> Rooted Ziggurat", player).access_rule = \
lambda state: (state.has(grapple, player) and has_ability(state, player, prayer, options, ability_unlocks)) \
or has_ice_grapple_logic(False, state, player, options, ability_unlocks)
multiworld.get_entrance("Quarry -> Rooted Ziggurat", player).access_rule = \
lambda state: has_ice_grapple_logic(False, state, player, options, ability_unlocks)
multiworld.get_entrance("Swamp -> Cathedral", player).access_rule = \
lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks) \
or has_ice_grapple_logic(False, state, player, options, ability_unlocks)
multiworld.get_entrance("Overworld -> Spirit Arena", player).access_rule = \
lambda state: (state.has(gold_hexagon, player, options.hexagon_goal.value) if options.hexagon_quest.value
else state.has_all({red_hexagon, green_hexagon, blue_hexagon}, player)) and \
has_ability(state, player, prayer, options, ability_unlocks) and has_sword(state, player)
def set_location_rules(world: "TunicWorld", ability_unlocks: Dict[str, int]) -> None:
multiworld = world.multiworld
player = world.player
options = world.options
forbid_item(multiworld.get_location("Secret Gathering Place - 20 Fairy Reward", player), fairies, player)
# Ability Shuffle Exclusive Rules
set_rule(multiworld.get_location("Far Shore - Page Pickup", player),
lambda state: has_ability(state, player, prayer, options, ability_unlocks))
set_rule(multiworld.get_location("Fortress Courtyard - Chest Near Cave", player),
lambda state: has_ability(state, player, prayer, options, ability_unlocks) or state.has(laurels, player)
or can_ladder_storage(state, player, options)
or (has_ice_grapple_logic(True, state, player, options, ability_unlocks)
and has_lantern(state, player, options)))
set_rule(multiworld.get_location("Fortress Courtyard - Page Near Cave", player),
lambda state: has_ability(state, player, prayer, options, ability_unlocks) or state.has(laurels, player)
or can_ladder_storage(state, player, options)
or (has_ice_grapple_logic(True, state, player, options, ability_unlocks)
and has_lantern(state, player, options)))
set_rule(multiworld.get_location("East Forest - Dancing Fox Spirit Holy Cross", player),
lambda state: has_ability(state, player, holy_cross, options, ability_unlocks))
set_rule(multiworld.get_location("Forest Grave Path - Holy Cross Code by Grave", player),
lambda state: has_ability(state, player, holy_cross, options, ability_unlocks))
set_rule(multiworld.get_location("East Forest - Golden Obelisk Holy Cross", player),
lambda state: has_ability(state, player, holy_cross, options, ability_unlocks))
set_rule(multiworld.get_location("Beneath the Well - [Powered Secret Room] Chest", player),
lambda state: has_ability(state, player, prayer, options, ability_unlocks))
set_rule(multiworld.get_location("West Garden - [North] Behind Holy Cross Door", player),
lambda state: has_ability(state, player, holy_cross, options, ability_unlocks))
set_rule(multiworld.get_location("Library Hall - Holy Cross Chest", player),
lambda state: has_ability(state, player, holy_cross, options, ability_unlocks))
set_rule(multiworld.get_location("Eastern Vault Fortress - [West Wing] Candles Holy Cross", player),
lambda state: has_ability(state, player, holy_cross, options, ability_unlocks))
set_rule(multiworld.get_location("West Garden - [Central Highlands] Holy Cross (Blue Lines)", player),
lambda state: has_ability(state, player, holy_cross, options, ability_unlocks))
set_rule(multiworld.get_location("Quarry - [Back Entrance] Bushes Holy Cross", player),
lambda state: has_ability(state, player, holy_cross, options, ability_unlocks))
set_rule(multiworld.get_location("Cathedral - Secret Legend Trophy Chest", player),
lambda state: has_ability(state, player, holy_cross, options, ability_unlocks))
# Overworld
set_rule(multiworld.get_location("Overworld - [Southwest] Fountain Page", player),
lambda state: state.has(laurels, player))
set_rule(multiworld.get_location("Overworld - [Southwest] Grapple Chest Over Walkway", player),
lambda state: state.has_any({grapple, laurels}, player))
set_rule(multiworld.get_location("Overworld - [Southwest] West Beach Guarded By Turret 2", player),
lambda state: state.has_any({grapple, laurels}, player))
set_rule(multiworld.get_location("Far Shore - Secret Chest", player),
lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks))
set_rule(multiworld.get_location("Overworld - [Southeast] Page on Pillar by Swamp", player),
lambda state: state.has(laurels, player))
set_rule(multiworld.get_location("Old House - Normal Chest", player),
lambda state: state.has(house_key, player)
or has_ice_grapple_logic(False, state, player, options, ability_unlocks)
or (state.has(laurels, player) and options.logic_rules))
set_rule(multiworld.get_location("Old House - Holy Cross Chest", player),
lambda state: has_ability(state, player, holy_cross, options, ability_unlocks) and
(state.has(house_key, player)
or has_ice_grapple_logic(False, state, player, options, ability_unlocks)
or (state.has(laurels, player) and options.logic_rules)))
set_rule(multiworld.get_location("Old House - Shield Pickup", player),
lambda state: state.has(house_key, player)
or has_ice_grapple_logic(False, state, player, options, ability_unlocks)
or (state.has(laurels, player) and options.logic_rules))
set_rule(multiworld.get_location("Overworld - [Northwest] Page on Pillar by Dark Tomb", player),
lambda state: state.has(laurels, player))
set_rule(multiworld.get_location("Overworld - [Southwest] From West Garden", player),
lambda state: state.has(laurels, player))
set_rule(multiworld.get_location("Overworld - [West] Chest After Bell", player),
lambda state: state.has(laurels, player)
or (has_lantern(state, player, options) and has_sword(state, player)))
set_rule(multiworld.get_location("Overworld - [Northwest] Chest Beneath Quarry Gate", player),
lambda state: state.has_any({grapple, laurels}, player) or options.logic_rules)
set_rule(multiworld.get_location("Overworld - [East] Grapple Chest", player),
lambda state: state.has(grapple, player))
set_rule(multiworld.get_location("Special Shop - Secret Page Pickup", player),
lambda state: state.has(laurels, player))
set_rule(multiworld.get_location("Sealed Temple - Holy Cross Chest", player),
lambda state: has_ability(state, player, holy_cross, options, ability_unlocks) and
(state.has(laurels, player)
or (has_lantern(state, player, options) and
(has_sword(state, player) or state.has(fire_wand, player)))
or has_ice_grapple_logic(False, state, player, options, ability_unlocks)))
set_rule(multiworld.get_location("Sealed Temple - Page Pickup", player),
lambda state: state.has(laurels, player)
or (has_lantern(state, player, options) and (has_sword(state, player) or state.has(fire_wand, player)))
or has_ice_grapple_logic(False, state, player, options, ability_unlocks))
set_rule(multiworld.get_location("Secret Gathering Place - 10 Fairy Reward", player),
lambda state: state.has(fairies, player, 10))
set_rule(multiworld.get_location("Secret Gathering Place - 20 Fairy Reward", player),
lambda state: state.has(fairies, player, 20))
set_rule(multiworld.get_location("Coins in the Well - 3 Coins", player),
lambda state: state.has(coins, player, 3))
set_rule(multiworld.get_location("Coins in the Well - 6 Coins", player),
lambda state: state.has(coins, player, 6))
set_rule(multiworld.get_location("Coins in the Well - 10 Coins", player),
lambda state: state.has(coins, player, 10))
set_rule(multiworld.get_location("Coins in the Well - 15 Coins", player),
lambda state: state.has(coins, player, 15))
# East Forest
set_rule(multiworld.get_location("East Forest - Lower Grapple Chest", player),
lambda state: state.has(grapple, player))
set_rule(multiworld.get_location("East Forest - Lower Dash Chest", player),
lambda state: state.has_all({grapple, laurels}, player))
set_rule(multiworld.get_location("East Forest - Ice Rod Grapple Chest", player),
lambda state: state.has_all({grapple, ice_dagger, fire_wand}, player)
and has_ability(state, player, ice_rod, options, ability_unlocks))
# West Garden
set_rule(multiworld.get_location("West Garden - [North] Across From Page Pickup", player),
lambda state: state.has(laurels, player))
set_rule(multiworld.get_location("West Garden - [West] In Flooded Walkway", player),
lambda state: state.has(laurels, player))
set_rule(multiworld.get_location("West Garden - [West Lowlands] Tree Holy Cross Chest", player),
lambda state: state.has(laurels, player)
and has_ability(state, player, holy_cross, options, ability_unlocks))
set_rule(multiworld.get_location("West Garden - [East Lowlands] Page Behind Ice Dagger House", player),
lambda state: (state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks))
or has_ice_grapple_logic(True, state, player, options, ability_unlocks))
set_rule(multiworld.get_location("West Garden - [Central Lowlands] Below Left Walkway", player),
lambda state: state.has(laurels, player))
set_rule(multiworld.get_location("West Garden - [Central Highlands] After Garden Knight", player),
lambda state: has_sword(state, player) or state.has(laurels, player)
or has_ice_grapple_logic(False, state, player, options, ability_unlocks)
or can_ladder_storage(state, player, options))
# Ruined Atoll
set_rule(multiworld.get_location("Ruined Atoll - [West] Near Kevin Block", player),
lambda state: state.has(laurels, player))
set_rule(multiworld.get_location("Ruined Atoll - [East] Locked Room Lower Chest", player),
lambda state: state.has_any({laurels, key}, player))
set_rule(multiworld.get_location("Ruined Atoll - [East] Locked Room Upper Chest", player),
lambda state: state.has_any({laurels, key}, player))
set_rule(multiworld.get_location("Librarian - Hexagon Green", player),
lambda state: has_sword(state, player) or options.logic_rules)
# Frog's Domain
set_rule(multiworld.get_location("Frog's Domain - Side Room Grapple Secret", player),
lambda state: state.has_any({grapple, laurels}, player))
set_rule(multiworld.get_location("Frog's Domain - Grapple Above Hot Tub", player),
lambda state: state.has_any({grapple, laurels}, player))
set_rule(multiworld.get_location("Frog's Domain - Escape Chest", player),
lambda state: state.has_any({grapple, laurels}, player))
# Eastern Vault Fortress
set_rule(multiworld.get_location("Fortress Leaf Piles - Secret Chest", player),
lambda state: state.has(laurels, player))
set_rule(multiworld.get_location("Fortress Arena - Siege Engine/Vault Key Pickup", player),
lambda state: has_sword(state, player) and
(has_ability(state, player, prayer, options, ability_unlocks)
or has_ice_grapple_logic(False, state, player, options, ability_unlocks)))
set_rule(multiworld.get_location("Fortress Arena - Hexagon Red", player),
lambda state: state.has(vault_key, player) and
(has_ability(state, player, prayer, options, ability_unlocks)
or has_ice_grapple_logic(False, state, player, options, ability_unlocks)))
# Beneath the Vault
set_rule(multiworld.get_location("Beneath the Fortress - Bridge", player),
lambda state: has_stick(state, player) or state.has_any({laurels, fire_wand}, player))
set_rule(multiworld.get_location("Beneath the Fortress - Obscured Behind Waterfall", player),
lambda state: has_stick(state, player) and has_lantern(state, player, options))
# Quarry
set_rule(multiworld.get_location("Quarry - [Central] Above Ladder Dash Chest", player),
lambda state: state.has(laurels, player))
set_rule(multiworld.get_location("Quarry - [West] Upper Area Bombable Wall", player),
lambda state: has_mask(state, player, options))
set_rule(multiworld.get_location("Rooted Ziggurat Lower - Hexagon Blue", player),
lambda state: has_sword(state, player))
# Swamp
set_rule(multiworld.get_location("Cathedral Gauntlet - Gauntlet Reward", player),
lambda state: state.has(laurels, player) and state.has(fire_wand, player) and has_sword(state, player))
set_rule(multiworld.get_location("Swamp - [Entrance] Above Entryway", player),
lambda state: state.has(laurels, player))
set_rule(multiworld.get_location("Swamp - [South Graveyard] Upper Walkway Dash Chest", player),
lambda state: state.has(laurels, player))
set_rule(multiworld.get_location("Swamp - [Outside Cathedral] Obscured Behind Memorial", player),
lambda state: state.has(laurels, player))
set_rule(multiworld.get_location("Swamp - [South Graveyard] 4 Orange Skulls", player),
lambda state: has_sword(state, player))
set_rule(multiworld.get_location("Swamp - [South Graveyard] Guarded By Tentacles", player),
lambda state: has_sword(state, player))
# Hero's Grave
set_rule(multiworld.get_location("Hero's Grave - Tooth Relic", player),
lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks))
set_rule(multiworld.get_location("Hero's Grave - Mushroom Relic", player),
lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks))
set_rule(multiworld.get_location("Hero's Grave - Ash Relic", player),
lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks))
set_rule(multiworld.get_location("Hero's Grave - Flowers Relic", player),
lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks))
set_rule(multiworld.get_location("Hero's Grave - Effigy Relic", player),
lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks))
set_rule(multiworld.get_location("Hero's Grave - Feathers Relic", player),
lambda state: state.has(laurels, player) and has_ability(state, player, prayer, options, ability_unlocks))

View File

@@ -0,0 +1,6 @@
from test.bases import WorldTestBase
class TunicTestBase(WorldTestBase):
game = "Tunic"
player: int = 1

View File

@@ -0,0 +1,70 @@
from . import TunicTestBase
from .. import options
class TestAccess(TunicTestBase):
# test whether you can get into the temple without laurels
def test_temple_access(self):
self.collect_all_but(["Hero's Laurels", "Lantern"])
self.assertFalse(self.can_reach_location("Sealed Temple - Page Pickup"))
self.collect_by_name(["Lantern"])
self.assertTrue(self.can_reach_location("Sealed Temple - Page Pickup"))
# test that the wells function properly. Since fairies is written the same way, that should succeed too
def test_wells(self):
self.collect_all_but(["Golden Coin"])
self.assertFalse(self.can_reach_location("Coins in the Well - 3 Coins"))
self.collect_by_name(["Golden Coin"])
self.assertTrue(self.can_reach_location("Coins in the Well - 3 Coins"))
class TestStandardShuffle(TunicTestBase):
options = {options.AbilityShuffling.internal_name: options.AbilityShuffling.option_true}
# test that you need to get holy cross to open the hc door in overworld
def test_hc_door(self):
self.assertFalse(self.can_reach_location("Fountain Cross Door - Page Pickup"))
self.collect_by_name("Pages 42-43 (Holy Cross)")
self.assertTrue(self.can_reach_location("Fountain Cross Door - Page Pickup"))
class TestHexQuestShuffle(TunicTestBase):
options = {options.HexagonQuest.internal_name: options.HexagonQuest.option_true,
options.AbilityShuffling.internal_name: options.AbilityShuffling.option_true}
# test that you need the gold questagons to open the hc door in overworld
def test_hc_door_hex_shuffle(self):
self.assertFalse(self.can_reach_location("Fountain Cross Door - Page Pickup"))
self.collect_by_name("Gold Questagon")
self.assertTrue(self.can_reach_location("Fountain Cross Door - Page Pickup"))
class TestHexQuestNoShuffle(TunicTestBase):
options = {options.HexagonQuest.internal_name: options.HexagonQuest.option_true,
options.AbilityShuffling.internal_name: options.AbilityShuffling.option_false}
# test that you can get the item behind the overworld hc door with nothing and no ability shuffle
def test_hc_door_no_shuffle(self):
self.assertTrue(self.can_reach_location("Fountain Cross Door - Page Pickup"))
class TestNormalGoal(TunicTestBase):
options = {options.HexagonQuest.internal_name: options.HexagonQuest.option_false}
# test that you need the three colored hexes to reach the Heir in standard
def test_normal_goal(self):
location = ["The Heir"]
items = [["Red Questagon", "Blue Questagon", "Green Questagon"]]
self.assertAccessDependency(location, items)
class TestER(TunicTestBase):
options = {options.EntranceRando.internal_name: options.EntranceRando.option_true,
options.AbilityShuffling.internal_name: options.AbilityShuffling.option_true,
options.HexagonQuest.internal_name: options.HexagonQuest.option_false}
def test_overworld_hc_chest(self):
# test to see that static connections are working properly -- this chest requires holy cross and is in Overworld
self.assertFalse(self.can_reach_location("Overworld - [Southwest] Flowers Holy Cross"))
self.collect_by_name(["Pages 42-43 (Holy Cross)"])
self.assertTrue(self.can_reach_location("Overworld - [Southwest] Flowers Holy Cross"))

View File

@@ -4,7 +4,6 @@
- Undertale from the [Steam page](https://store.steampowered.com/app/391540)
- Archipelago from the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases)
- (select `Undertale Client` during installation.)
### First time setup

View File

@@ -4,7 +4,7 @@ import functools
import settings
import threading
import typing
from typing import Any, Dict, List, Literal, Set, Tuple, Optional, cast
from typing import Any, Dict, List, Set, Tuple, Optional, cast
import os
import logging
@@ -12,7 +12,7 @@ from BaseClasses import ItemClassification, LocationProgressType, \
MultiWorld, Item, CollectionState, Entrance, Tutorial
from .logic import cs_to_zz_locs
from .region import ZillionLocation, ZillionRegion
from .options import ZillionOptions, ZillionStartChar, validate
from .options import ZillionOptions, validate
from .id_maps import item_name_to_id as _item_name_to_id, \
loc_name_to_id as _loc_name_to_id, make_id_to_others, \
zz_reg_name_to_reg_name, base_id
@@ -225,7 +225,7 @@ class ZillionWorld(World):
loc.access_rule = access_rule
if not (limited_skill >= zz_loc.req):
loc.progress_type = LocationProgressType.EXCLUDED
self.multiworld.exclude_locations[p].value.add(loc.name)
self.options.exclude_locations.value.add(loc.name)
here.locations.append(loc)
self.my_locations.append(loc)
@@ -288,15 +288,15 @@ class ZillionWorld(World):
if group["game"] == "Zillion":
assert "item_pool" in group
item_pool = group["item_pool"]
to_stay: Literal['Apple', 'Champ', 'JJ'] = "JJ"
to_stay: Chars = "JJ"
if "JJ" in item_pool:
assert "players" in group
group_players = group["players"]
start_chars = cast(Dict[int, ZillionStartChar], getattr(multiworld, "start_char"))
players_start_chars = [
(player, start_chars[player].current_option_name)
for player in group_players
]
players_start_chars: List[Tuple[int, Chars]] = []
for player in group_players:
z_world = multiworld.worlds[player]
assert isinstance(z_world, ZillionWorld)
players_start_chars.append((player, z_world.options.start_char.get_char()))
start_char_counts = Counter(sc for _, sc in players_start_chars)
# majority rules
if start_char_counts["Apple"] > start_char_counts["Champ"]:
@@ -304,7 +304,7 @@ class ZillionWorld(World):
elif start_char_counts["Champ"] > start_char_counts["Apple"]:
to_stay = "Champ"
else: # equal
choices: Tuple[Literal['Apple', 'Champ', 'JJ'], ...] = ("Apple", "Champ")
choices: Tuple[Chars, ...] = ("Apple", "Champ")
to_stay = multiworld.random.choice(choices)
for p, sc in players_start_chars:

View File

@@ -16,7 +16,7 @@ from zilliandomizer.utils.loc_name_maps import id_to_loc
from zilliandomizer.options import Chars
from zilliandomizer.patch import RescueInfo
from .id_maps import make_id_to_others
from .id_maps import loc_name_to_id, make_id_to_others
from .config import base_id, zillion_map
@@ -323,6 +323,7 @@ class ZillionContext(CommonContext):
elif isinstance(event_from_game, events.WinEventFromGame):
if not self.finished_game:
async_start(self.send_msgs([
{"cmd": 'LocationChecks', "locations": [loc_name_to_id["J-6 bottom far left"]]},
{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}
]))
self.finished_game = True

View File

@@ -2,7 +2,7 @@
## Required Software
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). Make sure to check the box for `Zillion Client - Zillion Patch Setup`
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases).
- RetroArch 1.10.3 or newer from: [RetroArch Website](https://retroarch.com?page=platforms).
@@ -30,9 +30,10 @@ Put your Zillion ROM file in the Archipelago directory in your home directory.
### Windows Setup
1. During the installation of Archipelago, install the Zillion Client. If you did not do this,
or you are on an older version, you may run the installer again to install the Zillion Client.
2. During setup, you will be asked to locate your base ROM file. This is the Zillion ROM file mentioned above in Required Software.
1. Download and install [Archipelago](<https://github.com/ArchipelagoMW/Archipelago/releases/latest>). **The installer
file is located in the assets section at the bottom of the version information.**
2. The first time you do local generation or patch your game, you will be asked to locate your base ROM file.
This is the Zillion ROM file mentioned above in Required Software. This only needs to be done once.
---
# Play

View File

@@ -1,9 +1,11 @@
from typing import Dict, FrozenSet, Tuple, cast, List, Counter as _Counter
from typing import Dict, FrozenSet, Tuple, List, Counter as _Counter
from BaseClasses import CollectionState
from zilliandomizer.logic_components.items import Item, items
from zilliandomizer.logic_components.locations import Location
from zilliandomizer.randomizer import Randomizer
from zilliandomizer.logic_components.items import Item, items
from .region import ZillionLocation
from .item import ZillionItem
from .id_maps import item_name_to_id
@@ -18,11 +20,12 @@ def set_randomizer_locs(cs: CollectionState, p: int, zz_r: Randomizer) -> int:
returns a hash of the player and of the set locations with their items
"""
from . import ZillionWorld
z_world = cs.multiworld.worlds[p]
my_locations = cast(List[ZillionLocation], getattr(z_world, "my_locations"))
assert isinstance(z_world, ZillionWorld)
_hash = p
for z_loc in my_locations:
for z_loc in z_world.my_locations:
zz_name = z_loc.zz_loc.name
zz_item = z_loc.item.zz_item \
if isinstance(z_loc.item, ZillionItem) and z_loc.item.player == p \

View File

@@ -1,13 +1,14 @@
from collections import Counter
from dataclasses import dataclass
from typing import Dict, Tuple
from typing import ClassVar, Dict, Tuple
from typing_extensions import TypeGuard # remove when Python >= 3.10
from Options import DefaultOnToggle, NamedRange, PerGameCommonOptions, Range, Toggle, Choice
from zilliandomizer.options import \
Options as ZzOptions, char_to_gun, char_to_jump, ID, \
VBLR as ZzVBLR, chars, Chars, ItemCounts as ZzItemCounts
from zilliandomizer.options import (
Options as ZzOptions, char_to_gun, char_to_jump, ID,
VBLR as ZzVBLR, Chars, ItemCounts as ZzItemCounts
)
from zilliandomizer.options.parsing import validate as zz_validate
@@ -107,6 +108,15 @@ class ZillionStartChar(Choice):
display_name = "start character"
default = "random"
_name_capitalization: ClassVar[Dict[int, Chars]] = {
option_jj: "JJ",
option_apple: "Apple",
option_champ: "Champ",
}
def get_char(self) -> Chars:
return ZillionStartChar._name_capitalization[self.value]
class ZillionIDCardCount(Range):
"""
@@ -348,16 +358,6 @@ def validate(options: ZillionOptions) -> "Tuple[ZzOptions, Counter[str]]":
# that should be all of the level requirements met
name_capitalization: Dict[str, Chars] = {
"jj": "JJ",
"apple": "Apple",
"champ": "Champ",
}
start_char = options.start_char
start_char_name = name_capitalization[start_char.current_key]
assert start_char_name in chars
starting_cards = options.starting_cards
room_gen = options.room_gen
@@ -371,7 +371,7 @@ def validate(options: ZillionOptions) -> "Tuple[ZzOptions, Counter[str]]":
max_level.value,
False, # tutorial
skill,
start_char_name,
options.start_char.get_char(),
floppy_req.value,
options.continues.value,
bool(options.randomize_alarms.value),

View File

@@ -3,3 +3,11 @@
This folder is for already merged worlds that are unmaintained and currently broken. If you are interested in fixing and
stepping up as maintainer for any of these worlds, please review the [world maintainer](/docs/world%20maintainer.md)
documentation.
## Information for Disabled Worlds
For each disabled world, a README file can be found detailing when the world was disabled and the reasons that it
was disabled. In order to be considered for reactivation, these concerns should be handled at a bare minimum. However,
each world may have additional issues that also need to be handled, such as deprecated API calls or missing components.

Some files were not shown because too many files have changed in this diff Show More