mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-07 15:13:52 -08:00
Compare commits
37 Commits
NewSoupVi-
...
NewSoupVi-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
b7e644e2af | ||
|
|
334781e976 | ||
|
|
6c939d2d59 | ||
|
|
e882c68277 | ||
|
|
dbf284d4b2 | ||
|
|
75624042f7 | ||
|
|
0dade05133 | ||
|
|
fcaba14b62 | ||
|
|
6073d5e37e | ||
|
|
41a7d7eeee | ||
|
|
d3a3c29bc9 | ||
|
|
0ad5b0ade8 | ||
|
|
e6e31a27e6 | ||
|
|
a650e90b57 | ||
|
|
36f17111bf | ||
|
|
03b90cf39b | ||
|
|
5729b78504 | ||
|
|
ba50c947ba | ||
|
|
2424b79626 | ||
|
|
d4b1351c99 | ||
|
|
859ae87ec9 | ||
|
|
124ce13da7 | ||
|
|
48ea274655 | ||
|
|
85a713771b | ||
|
|
3ae8992fb6 | ||
|
|
01c6037562 | ||
|
|
4b80b786e2 | ||
|
|
bd5c8ec172 | ||
|
|
baf291d7a2 | ||
|
|
9c102da901 | ||
|
|
75e18e3cc9 | ||
|
|
a3d6036939 | ||
|
|
7eb12174b7 | ||
|
|
73146ef30c | ||
|
|
66314de965 | ||
|
|
5141f36e95 | ||
|
|
9ba613277e |
2
.github/pyright-config.json
vendored
2
.github/pyright-config.json
vendored
@@ -16,7 +16,7 @@
|
||||
"reportMissingImports": true,
|
||||
"reportMissingTypeStubs": true,
|
||||
|
||||
"pythonVersion": "3.8",
|
||||
"pythonVersion": "3.10",
|
||||
"pythonPlatform": "Windows",
|
||||
|
||||
"executionEnvironments": [
|
||||
|
||||
2
.github/workflows/analyze-modified-files.yml
vendored
2
.github/workflows/analyze-modified-files.yml
vendored
@@ -53,7 +53,7 @@ jobs:
|
||||
- uses: actions/setup-python@v5
|
||||
if: env.diff != ''
|
||||
with:
|
||||
python-version: 3.8
|
||||
python-version: '3.10'
|
||||
|
||||
- name: "Install dependencies"
|
||||
if: env.diff != ''
|
||||
|
||||
4
.github/workflows/build.yml
vendored
4
.github/workflows/build.yml
vendored
@@ -24,14 +24,14 @@ env:
|
||||
jobs:
|
||||
# build-release-macos: # LF volunteer
|
||||
|
||||
build-win-py38: # RCs will still be built and signed by hand
|
||||
build-win-py310: # RCs will still be built and signed by hand
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.8'
|
||||
python-version: '3.10'
|
||||
- name: Download run-time dependencies
|
||||
run: |
|
||||
Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/${Env:ENEMIZER_VERSION}/win-x64.zip -OutFile enemizer.zip
|
||||
|
||||
4
.github/workflows/unittests.yml
vendored
4
.github/workflows/unittests.yml
vendored
@@ -33,13 +33,11 @@ jobs:
|
||||
matrix:
|
||||
os: [ubuntu-latest]
|
||||
python:
|
||||
- {version: '3.8'}
|
||||
- {version: '3.9'}
|
||||
- {version: '3.10'}
|
||||
- {version: '3.11'}
|
||||
- {version: '3.12'}
|
||||
include:
|
||||
- python: {version: '3.8'} # win7 compat
|
||||
- python: {version: '3.10'} # old compat
|
||||
os: windows-latest
|
||||
- python: {version: '3.12'} # current
|
||||
os: windows-latest
|
||||
|
||||
@@ -1,18 +1,16 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import collections
|
||||
import itertools
|
||||
import functools
|
||||
import logging
|
||||
import random
|
||||
import secrets
|
||||
import typing # this can go away when Python 3.8 support is dropped
|
||||
from argparse import Namespace
|
||||
from collections import Counter, deque
|
||||
from collections.abc import Collection, MutableSequence
|
||||
from enum import IntEnum, IntFlag
|
||||
from typing import (AbstractSet, Any, Callable, ClassVar, Dict, Iterable, Iterator, List, Mapping, NamedTuple,
|
||||
Optional, Protocol, Set, Tuple, Union, Type)
|
||||
Optional, Protocol, Set, Tuple, Union, TYPE_CHECKING)
|
||||
|
||||
from typing_extensions import NotRequired, TypedDict
|
||||
|
||||
@@ -20,7 +18,7 @@ import NetUtils
|
||||
import Options
|
||||
import Utils
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
if TYPE_CHECKING:
|
||||
from worlds import AutoWorld
|
||||
|
||||
|
||||
@@ -231,7 +229,7 @@ class MultiWorld():
|
||||
for player in self.player_ids:
|
||||
world_type = AutoWorld.AutoWorldRegister.world_types[self.game[player]]
|
||||
self.worlds[player] = world_type(self, player)
|
||||
options_dataclass: typing.Type[Options.PerGameCommonOptions] = world_type.options_dataclass
|
||||
options_dataclass: type[Options.PerGameCommonOptions] = world_type.options_dataclass
|
||||
self.worlds[player].options = options_dataclass(**{option_key: getattr(args, option_key)[player]
|
||||
for option_key in options_dataclass.type_hints})
|
||||
|
||||
@@ -975,7 +973,7 @@ class Region:
|
||||
entrances: List[Entrance]
|
||||
exits: List[Entrance]
|
||||
locations: List[Location]
|
||||
entrance_type: ClassVar[Type[Entrance]] = Entrance
|
||||
entrance_type: ClassVar[type[Entrance]] = Entrance
|
||||
|
||||
class Register(MutableSequence):
|
||||
region_manager: MultiWorld.RegionManager
|
||||
@@ -1075,7 +1073,7 @@ class Region:
|
||||
return entrance.parent_region.get_connecting_entrance(is_main_entrance)
|
||||
|
||||
def add_locations(self, locations: Dict[str, Optional[int]],
|
||||
location_type: Optional[Type[Location]] = None) -> None:
|
||||
location_type: Optional[type[Location]] = None) -> None:
|
||||
"""
|
||||
Adds locations to the Region object, where location_type is your Location class and locations is a dict of
|
||||
location names to address.
|
||||
|
||||
@@ -710,6 +710,11 @@ class CommonContext:
|
||||
|
||||
def run_cli(self):
|
||||
if sys.stdin:
|
||||
if sys.stdin.fileno() != 0:
|
||||
from multiprocessing import parent_process
|
||||
if parent_process():
|
||||
return # ignore MultiProcessing pipe
|
||||
|
||||
# steam overlay breaks when starting console_loop
|
||||
if 'gameoverlayrenderer' in os.environ.get('LD_PRELOAD', ''):
|
||||
logger.info("Skipping terminal input, due to conflicting Steam Overlay detected. Please use GUI only.")
|
||||
|
||||
@@ -453,6 +453,10 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
|
||||
raise Exception(f"Option {option_key} has to be in a game's section, not on its own.")
|
||||
|
||||
ret.game = get_choice("game", weights)
|
||||
if not isinstance(ret.game, str):
|
||||
if ret.game is None:
|
||||
raise Exception('"game" not specified')
|
||||
raise Exception(f"Invalid game: {ret.game}")
|
||||
if ret.game not in AutoWorldRegister.world_types:
|
||||
from worlds import failed_world_loads
|
||||
picks = Utils.get_fuzzy_results(ret.game, list(AutoWorldRegister.world_types) + failed_world_loads, limit=1)[0]
|
||||
|
||||
16
Launcher.py
16
Launcher.py
@@ -22,16 +22,15 @@ from os.path import isfile
|
||||
from shutil import which
|
||||
from typing import Callable, Optional, Sequence, Tuple, Union
|
||||
|
||||
import Utils
|
||||
import settings
|
||||
from worlds.LauncherComponents import Component, components, Type, SuffixIdentifier, icon_paths
|
||||
|
||||
if __name__ == "__main__":
|
||||
import ModuleUpdate
|
||||
ModuleUpdate.update()
|
||||
|
||||
from Utils import is_frozen, user_path, local_path, init_logging, open_filename, messagebox, \
|
||||
is_windows, is_macos, is_linux
|
||||
import settings
|
||||
import Utils
|
||||
from Utils import (init_logging, is_frozen, is_linux, is_macos, is_windows, local_path, messagebox, open_filename,
|
||||
user_path)
|
||||
from worlds.LauncherComponents import Component, components, icon_paths, SuffixIdentifier, Type
|
||||
|
||||
|
||||
def open_host_yaml():
|
||||
@@ -182,6 +181,11 @@ def handle_uri(path: str, launch_args: Tuple[str, ...]) -> None:
|
||||
App.get_running_app().stop()
|
||||
Window.close()
|
||||
|
||||
def _stop(self, *largs):
|
||||
# see run_gui Launcher _stop comment for details
|
||||
self.root_window.close()
|
||||
super()._stop(*largs)
|
||||
|
||||
Popup().run()
|
||||
|
||||
|
||||
|
||||
@@ -5,8 +5,8 @@ import multiprocessing
|
||||
import warnings
|
||||
|
||||
|
||||
if sys.version_info < (3, 8, 6):
|
||||
raise RuntimeError("Incompatible Python Version. 3.8.7+ is supported.")
|
||||
if sys.version_info < (3, 10, 11):
|
||||
raise RuntimeError(f"Incompatible Python Version found: {sys.version_info}. 3.10.11+ is supported.")
|
||||
|
||||
# don't run update if environment is frozen/compiled or if not the parent process (skip in subprocess)
|
||||
_skip_update = bool(getattr(sys, "frozen", False) or multiprocessing.parent_process())
|
||||
|
||||
@@ -727,15 +727,15 @@ class Context:
|
||||
if not hint.local and data not in concerns[hint.finding_player]:
|
||||
concerns[hint.finding_player].append(data)
|
||||
# remember hints in all cases
|
||||
if not hint.found:
|
||||
# since hints are bidirectional, finding player and receiving player,
|
||||
# we can check once if hint already exists
|
||||
if hint not in self.hints[team, hint.finding_player]:
|
||||
self.hints[team, hint.finding_player].add(hint)
|
||||
new_hint_events.add(hint.finding_player)
|
||||
for player in self.slot_set(hint.receiving_player):
|
||||
self.hints[team, player].add(hint)
|
||||
new_hint_events.add(player)
|
||||
|
||||
# since hints are bidirectional, finding player and receiving player,
|
||||
# we can check once if hint already exists
|
||||
if hint not in self.hints[team, hint.finding_player]:
|
||||
self.hints[team, hint.finding_player].add(hint)
|
||||
new_hint_events.add(hint.finding_player)
|
||||
for player in self.slot_set(hint.receiving_player):
|
||||
self.hints[team, player].add(hint)
|
||||
new_hint_events.add(player)
|
||||
|
||||
self.logger.info("Notice (Team #%d): %s" % (team + 1, format_hint(self, team, hint)))
|
||||
for slot in new_hint_events:
|
||||
|
||||
8
Utils.py
8
Utils.py
@@ -18,8 +18,8 @@ import warnings
|
||||
|
||||
from argparse import Namespace
|
||||
from settings import Settings, get_settings
|
||||
from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union
|
||||
from typing_extensions import TypeGuard
|
||||
from time import sleep
|
||||
from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union, TypeGuard
|
||||
from yaml import load, load_all, dump
|
||||
|
||||
try:
|
||||
@@ -47,7 +47,7 @@ class Version(typing.NamedTuple):
|
||||
return ".".join(str(item) for item in self)
|
||||
|
||||
|
||||
__version__ = "0.5.1"
|
||||
__version__ = "0.6.0"
|
||||
version_tuple = tuplize_version(__version__)
|
||||
|
||||
is_linux = sys.platform.startswith("linux")
|
||||
@@ -568,6 +568,8 @@ def stream_input(stream: typing.TextIO, queue: "asyncio.Queue[str]"):
|
||||
else:
|
||||
if text:
|
||||
queue.put_nowait(text)
|
||||
else:
|
||||
sleep(0.01) # non-blocking stream
|
||||
|
||||
from threading import Thread
|
||||
thread = Thread(target=queuer, name=f"Stream handler for {stream.name}", daemon=True)
|
||||
|
||||
@@ -17,7 +17,7 @@ from Utils import get_file_safe_name
|
||||
if typing.TYPE_CHECKING:
|
||||
from flask import Flask
|
||||
|
||||
Utils.local_path.cached_path = os.path.dirname(__file__) or "." # py3.8 is not abs. remove "." when dropping 3.8
|
||||
Utils.local_path.cached_path = os.path.dirname(__file__)
|
||||
settings.no_gui = True
|
||||
configpath = os.path.abspath("config.yaml")
|
||||
if not os.path.exists(configpath): # fall back to config.yaml in home
|
||||
|
||||
@@ -5,9 +5,7 @@ waitress>=3.0.0
|
||||
Flask-Caching>=2.3.0
|
||||
Flask-Compress>=1.15
|
||||
Flask-Limiter>=3.8.0
|
||||
bokeh>=3.1.1; python_version <= '3.8'
|
||||
bokeh>=3.4.3; python_version == '3.9'
|
||||
bokeh>=3.5.2; python_version >= '3.10'
|
||||
bokeh>=3.5.2
|
||||
markupsafe>=2.1.5
|
||||
Markdown>=3.7
|
||||
mdx-breakless-lists>=1.0.1
|
||||
|
||||
@@ -98,6 +98,8 @@
|
||||
<td>
|
||||
{% if hint.finding_player == player %}
|
||||
<b>{{ player_names_with_alias[(team, hint.finding_player)] }}</b>
|
||||
{% elif get_slot_info(team, hint.finding_player).type == 2 %}
|
||||
<i>{{ player_names_with_alias[(team, hint.finding_player)] }}</i>
|
||||
{% else %}
|
||||
<a href="{{ url_for("get_player_tracker", tracker=room.tracker, tracked_team=team, tracked_player=hint.finding_player) }}">
|
||||
{{ player_names_with_alias[(team, hint.finding_player)] }}
|
||||
@@ -107,6 +109,8 @@
|
||||
<td>
|
||||
{% if hint.receiving_player == player %}
|
||||
<b>{{ player_names_with_alias[(team, hint.receiving_player)] }}</b>
|
||||
{% elif get_slot_info(team, hint.receiving_player).type == 2 %}
|
||||
<i>{{ player_names_with_alias[(team, hint.receiving_player)] }}</i>
|
||||
{% else %}
|
||||
<a href="{{ url_for("get_player_tracker", tracker=room.tracker, tracked_team=team, tracked_player=hint.receiving_player) }}">
|
||||
{{ player_names_with_alias[(team, hint.receiving_player)] }}
|
||||
|
||||
@@ -21,8 +21,20 @@
|
||||
)
|
||||
-%}
|
||||
<tr>
|
||||
<td>{{ player_names_with_alias[(team, hint.finding_player)] }}</td>
|
||||
<td>{{ player_names_with_alias[(team, hint.receiving_player)] }}</td>
|
||||
<td>
|
||||
{% if get_slot_info(team, hint.finding_player).type == 2 %}
|
||||
<i>{{ player_names_with_alias[(team, hint.finding_player)] }}</i>
|
||||
{% else %}
|
||||
{{ player_names_with_alias[(team, hint.finding_player)] }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>
|
||||
{% if get_slot_info(team, hint.receiving_player).type == 2 %}
|
||||
<i>{{ player_names_with_alias[(team, hint.receiving_player)] }}</i>
|
||||
{% else %}
|
||||
{{ player_names_with_alias[(team, hint.receiving_player)] }}
|
||||
{% endif %}
|
||||
</td>
|
||||
<td>{{ item_id_to_name[games[(team, hint.receiving_player)]][hint.item] }}</td>
|
||||
<td>{{ location_id_to_name[games[(team, hint.finding_player)]][hint.location] }}</td>
|
||||
<td>{{ games[(team, hint.finding_player)] }}</td>
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
<table class="range-rows" data-option="{{ option_name }}">
|
||||
<tbody>
|
||||
{{ RangeRow(option_name, option, option.range_start, option.range_start, True) }}
|
||||
{% if option.range_start < option.default < option.range_end %}
|
||||
{% if option.default is number and option.range_start < option.default < option.range_end %}
|
||||
{{ RangeRow(option_name, option, option.default, option.default, True) }}
|
||||
{% endif %}
|
||||
{{ RangeRow(option_name, option, option.range_end, option.range_end, True) }}
|
||||
|
||||
@@ -423,6 +423,7 @@ def render_generic_tracker(tracker_data: TrackerData, team: int, player: int) ->
|
||||
template_name_or_list="genericTracker.html",
|
||||
game_specific_tracker=game in _player_trackers,
|
||||
room=tracker_data.room,
|
||||
get_slot_info=tracker_data.get_slot_info,
|
||||
team=team,
|
||||
player=player,
|
||||
player_name=tracker_data.get_room_long_player_names()[team, player],
|
||||
@@ -446,6 +447,7 @@ def render_generic_multiworld_tracker(tracker_data: TrackerData, enabled_tracker
|
||||
enabled_trackers=enabled_trackers,
|
||||
current_tracker="Generic",
|
||||
room=tracker_data.room,
|
||||
get_slot_info=tracker_data.get_slot_info,
|
||||
all_slots=tracker_data.get_all_slots(),
|
||||
room_players=tracker_data.get_all_players(),
|
||||
locations=tracker_data.get_room_locations(),
|
||||
@@ -497,7 +499,7 @@ if "Factorio" in network_data_package["games"]:
|
||||
(team, player): collections.Counter({
|
||||
tracker_data.item_id_to_name["Factorio"][item_id]: count
|
||||
for item_id, count in tracker_data.get_player_inventory_counts(team, player).items()
|
||||
}) for team, players in tracker_data.get_all_slots().items() for player in players
|
||||
}) for team, players in tracker_data.get_all_players().items() for player in players
|
||||
if tracker_data.get_player_game(team, player) == "Factorio"
|
||||
}
|
||||
|
||||
@@ -506,6 +508,7 @@ if "Factorio" in network_data_package["games"]:
|
||||
enabled_trackers=enabled_trackers,
|
||||
current_tracker="Factorio",
|
||||
room=tracker_data.room,
|
||||
get_slot_info=tracker_data.get_slot_info,
|
||||
all_slots=tracker_data.get_all_slots(),
|
||||
room_players=tracker_data.get_all_players(),
|
||||
locations=tracker_data.get_room_locations(),
|
||||
@@ -638,6 +641,7 @@ 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,
|
||||
get_slot_info=tracker_data.get_slot_info,
|
||||
all_slots=tracker_data.get_all_slots(),
|
||||
room_players=tracker_data.get_all_players(),
|
||||
locations=tracker_data.get_room_locations(),
|
||||
|
||||
@@ -16,7 +16,7 @@ game contributions:
|
||||
* **Do not introduce unit test failures/regressions.**
|
||||
Archipelago supports multiple versions of Python. You may need to download older Python versions to fully test
|
||||
your changes. Currently, the oldest supported version
|
||||
is [Python 3.8](https://www.python.org/downloads/release/python-380/).
|
||||
is [Python 3.10](https://www.python.org/downloads/release/python-31015/).
|
||||
It is recommended that automated github actions are turned on in your fork to have github run unit tests after
|
||||
pushing.
|
||||
You can turn them on here:
|
||||
|
||||
@@ -7,7 +7,7 @@ use that version. These steps are for developers or platforms without compiled r
|
||||
## General
|
||||
|
||||
What you'll need:
|
||||
* [Python 3.8.7 or newer](https://www.python.org/downloads/), not the Windows Store version
|
||||
* [Python 3.10.15 or newer](https://www.python.org/downloads/), not the Windows Store version
|
||||
* Python 3.12.x is currently the newest supported version
|
||||
* pip: included in downloads from python.org, separate in many Linux distributions
|
||||
* Matching C compiler
|
||||
|
||||
5
kvui.py
5
kvui.py
@@ -12,10 +12,7 @@ if sys.platform == "win32":
|
||||
|
||||
# kivy 2.2.0 introduced DPI awareness on Windows, but it makes the UI enter an infinitely recursive re-layout
|
||||
# by setting the application to not DPI Aware, Windows handles scaling the entire window on its own, ignoring kivy's
|
||||
try:
|
||||
ctypes.windll.shcore.SetProcessDpiAwareness(0)
|
||||
except FileNotFoundError: # shcore may not be found on <= Windows 7
|
||||
pass # TODO: remove silent except when Python 3.8 is phased out.
|
||||
ctypes.windll.shcore.SetProcessDpiAwareness(0)
|
||||
|
||||
os.environ["KIVY_NO_CONSOLELOG"] = "1"
|
||||
os.environ["KIVY_NO_FILELOG"] = "1"
|
||||
|
||||
2
setup.py
2
setup.py
@@ -634,7 +634,7 @@ cx_Freeze.setup(
|
||||
"excludes": ["numpy", "Cython", "PySide2", "PIL",
|
||||
"pandas", "zstandard"],
|
||||
"zip_include_packages": ["*"],
|
||||
"zip_exclude_packages": ["worlds", "sc2", "orjson"], # TODO: remove orjson here once we drop py3.8 support
|
||||
"zip_exclude_packages": ["worlds", "sc2"],
|
||||
"include_files": [], # broken in cx 6.14.0, we use more special sauce now
|
||||
"include_msvcr": False,
|
||||
"replace_paths": ["*."],
|
||||
|
||||
@@ -78,4 +78,4 @@ class TestOptions(unittest.TestCase):
|
||||
if not world_type.hidden:
|
||||
for option_key, option in world_type.options_dataclass.type_hints.items():
|
||||
with self.subTest(game=gamename, option=option_key):
|
||||
pickle.dumps(option(option.default))
|
||||
pickle.dumps(option.from_any(option.default))
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import unittest
|
||||
|
||||
from BaseClasses import PlandoOptions
|
||||
from worlds import AutoWorldRegister
|
||||
from Options import ItemDict, NamedRange, NumericOption, OptionList, OptionSet
|
||||
|
||||
@@ -14,6 +15,10 @@ class TestOptionPresets(unittest.TestCase):
|
||||
with self.subTest(game=game_name, preset=preset_name, option=option_name):
|
||||
try:
|
||||
option = world_type.options_dataclass.type_hints[option_name].from_any(option_value)
|
||||
# some options may need verification to ensure the provided option is actually valid
|
||||
# pass in all plando options in case a preset wants to require certain plando options
|
||||
# for some reason
|
||||
option.verify(world_type, "Test Player", PlandoOptions(sum(PlandoOptions)))
|
||||
supported_types = [NumericOption, OptionSet, OptionList, ItemDict]
|
||||
if not any([issubclass(option.__class__, t) for t in supported_types]):
|
||||
self.fail(f"'{option_name}' in preset '{preset_name}' for game '{game_name}' "
|
||||
|
||||
@@ -2,9 +2,7 @@
|
||||
from __future__ import annotations
|
||||
import abc
|
||||
import logging
|
||||
from typing import TYPE_CHECKING, ClassVar, Dict, Iterable, Tuple, Any, Optional, Union
|
||||
|
||||
from typing_extensions import TypeGuard
|
||||
from typing import TYPE_CHECKING, ClassVar, Dict, Iterable, Tuple, Any, Optional, Union, TypeGuard
|
||||
|
||||
from worlds.LauncherComponents import Component, SuffixIdentifier, Type, components
|
||||
|
||||
|
||||
@@ -66,19 +66,12 @@ class WorldSource:
|
||||
start = time.perf_counter()
|
||||
if self.is_zip:
|
||||
importer = zipimport.zipimporter(self.resolved_path)
|
||||
if hasattr(importer, "find_spec"): # new in Python 3.10
|
||||
spec = importer.find_spec(os.path.basename(self.path).rsplit(".", 1)[0])
|
||||
assert spec, f"{self.path} is not a loadable module"
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
else: # TODO: remove with 3.8 support
|
||||
mod = importer.load_module(os.path.basename(self.path).rsplit(".", 1)[0])
|
||||
spec = importer.find_spec(os.path.basename(self.path).rsplit(".", 1)[0])
|
||||
assert spec, f"{self.path} is not a loadable module"
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
|
||||
mod.__package__ = f"worlds.{mod.__package__}"
|
||||
|
||||
if mod.__package__ is not None:
|
||||
mod.__package__ = f"worlds.{mod.__package__}"
|
||||
else:
|
||||
# load_module does not populate package, we'll have to assume mod.__name__ is correct here
|
||||
# probably safe to remove with 3.8 support
|
||||
mod.__package__ = f"worlds.{mod.__name__}"
|
||||
mod.__name__ = f"worlds.{mod.__name__}"
|
||||
sys.modules[mod.__name__] = mod
|
||||
with warnings.catch_warnings():
|
||||
|
||||
@@ -740,17 +740,20 @@ def is_valid_first_act(world: "HatInTimeWorld", act: Region) -> bool:
|
||||
|
||||
|
||||
def connect_time_rift(world: "HatInTimeWorld", time_rift: Region, exit_region: Region):
|
||||
i = 1
|
||||
while i <= len(rift_access_regions[time_rift.name]):
|
||||
for i, access_region in enumerate(rift_access_regions[time_rift.name], start=1):
|
||||
# Matches the naming convention and iteration order in `create_rift_connections()`.
|
||||
name = f"{time_rift.name} Portal - Entrance {i}"
|
||||
entrance: Entrance
|
||||
try:
|
||||
entrance = world.multiworld.get_entrance(name, world.player)
|
||||
entrance = world.get_entrance(name)
|
||||
# Reconnect the rift access region to the new exit region.
|
||||
reconnect_regions(entrance, entrance.parent_region, exit_region)
|
||||
except KeyError:
|
||||
time_rift.connect(exit_region, name)
|
||||
|
||||
i += 1
|
||||
# The original entrance to the time rift has been deleted by already reconnecting a telescope act to the
|
||||
# time rift, so create a new entrance from the original rift access region to the new exit region.
|
||||
# Normally, acts and time rifts are sorted such that time rifts are reconnected to acts/rifts first, but
|
||||
# starting acts/rifts and act-plando can reconnect acts to time rifts before this happens.
|
||||
world.get_region(access_region).connect(exit_region, name)
|
||||
|
||||
|
||||
def get_shuffleable_act_regions(world: "HatInTimeWorld") -> List[Region]:
|
||||
|
||||
@@ -1152,79 +1152,79 @@ class AquariaRegions:
|
||||
def __no_progression_hard_or_hidden_location(self) -> None:
|
||||
self.multiworld.get_location("Energy Temple boss area, Fallen God Tooth",
|
||||
self.player).item_rule = \
|
||||
lambda item: item.classification != ItemClassification.progression
|
||||
lambda item: not item.advancement
|
||||
self.multiworld.get_location("Mithalas boss area, beating Mithalan God",
|
||||
self.player).item_rule = \
|
||||
lambda item: item.classification != ItemClassification.progression
|
||||
lambda item: not item.advancement
|
||||
self.multiworld.get_location("Kelp Forest boss area, beating Drunian God",
|
||||
self.player).item_rule = \
|
||||
lambda item: item.classification != ItemClassification.progression
|
||||
lambda item: not item.advancement
|
||||
self.multiworld.get_location("Sun Temple boss area, beating Sun God",
|
||||
self.player).item_rule = \
|
||||
lambda item: item.classification != ItemClassification.progression
|
||||
lambda item: not item.advancement
|
||||
self.multiworld.get_location("Sunken City, bulb on top of the boss area",
|
||||
self.player).item_rule = \
|
||||
lambda item: item.classification != ItemClassification.progression
|
||||
lambda item: not item.advancement
|
||||
self.multiworld.get_location("Home Water, Nautilus Egg",
|
||||
self.player).item_rule = \
|
||||
lambda item: item.classification != ItemClassification.progression
|
||||
lambda item: not item.advancement
|
||||
self.multiworld.get_location("Energy Temple blaster room, Blaster Egg",
|
||||
self.player).item_rule = \
|
||||
lambda item: item.classification != ItemClassification.progression
|
||||
lambda item: not item.advancement
|
||||
self.multiworld.get_location("Mithalas City Castle, beating the Priests",
|
||||
self.player).item_rule = \
|
||||
lambda item: item.classification != ItemClassification.progression
|
||||
lambda item: not item.advancement
|
||||
self.multiworld.get_location("Mermog cave, Piranha Egg",
|
||||
self.player).item_rule = \
|
||||
lambda item: item.classification != ItemClassification.progression
|
||||
lambda item: not item.advancement
|
||||
self.multiworld.get_location("Octopus Cave, Dumbo Egg",
|
||||
self.player).item_rule = \
|
||||
lambda item: item.classification != ItemClassification.progression
|
||||
lambda item: not item.advancement
|
||||
self.multiworld.get_location("King Jellyfish Cave, bulb in the right path from King Jelly",
|
||||
self.player).item_rule = \
|
||||
lambda item: item.classification != ItemClassification.progression
|
||||
lambda item: not item.advancement
|
||||
self.multiworld.get_location("King Jellyfish Cave, Jellyfish Costume",
|
||||
self.player).item_rule = \
|
||||
lambda item: item.classification != ItemClassification.progression
|
||||
lambda item: not item.advancement
|
||||
self.multiworld.get_location("Final Boss area, bulb in the boss third form room",
|
||||
self.player).item_rule = \
|
||||
lambda item: item.classification != ItemClassification.progression
|
||||
lambda item: not item.advancement
|
||||
self.multiworld.get_location("Sun Worm path, first cliff bulb",
|
||||
self.player).item_rule = \
|
||||
lambda item: item.classification != ItemClassification.progression
|
||||
lambda item: not item.advancement
|
||||
self.multiworld.get_location("Sun Worm path, second cliff bulb",
|
||||
self.player).item_rule = \
|
||||
lambda item: item.classification != ItemClassification.progression
|
||||
lambda item: not item.advancement
|
||||
self.multiworld.get_location("The Veil top right area, bulb at the top of the waterfall",
|
||||
self.player).item_rule = \
|
||||
lambda item: item.classification != ItemClassification.progression
|
||||
lambda item: not item.advancement
|
||||
self.multiworld.get_location("Bubble Cave, bulb in the left cave wall",
|
||||
self.player).item_rule = \
|
||||
lambda item: item.classification != ItemClassification.progression
|
||||
lambda item: not item.advancement
|
||||
self.multiworld.get_location("Bubble Cave, bulb in the right cave wall (behind the ice crystal)",
|
||||
self.player).item_rule = \
|
||||
lambda item: item.classification != ItemClassification.progression
|
||||
lambda item: not item.advancement
|
||||
self.multiworld.get_location("Bubble Cave, Verse Egg",
|
||||
self.player).item_rule = \
|
||||
lambda item: item.classification != ItemClassification.progression
|
||||
lambda item: not item.advancement
|
||||
self.multiworld.get_location("Kelp Forest bottom left area, bulb close to the spirit crystals",
|
||||
self.player).item_rule = \
|
||||
lambda item: item.classification != ItemClassification.progression
|
||||
lambda item: not item.advancement
|
||||
self.multiworld.get_location("Kelp Forest bottom left area, Walker Baby",
|
||||
self.player).item_rule = \
|
||||
lambda item: item.classification != ItemClassification.progression
|
||||
lambda item: not item.advancement
|
||||
self.multiworld.get_location("Sun Temple, Sun Key",
|
||||
self.player).item_rule = \
|
||||
lambda item: item.classification != ItemClassification.progression
|
||||
lambda item: not item.advancement
|
||||
self.multiworld.get_location("The Body bottom area, Mutant Costume",
|
||||
self.player).item_rule = \
|
||||
lambda item: item.classification != ItemClassification.progression
|
||||
lambda item: not item.advancement
|
||||
self.multiworld.get_location("Sun Temple, bulb in the hidden room of the right part",
|
||||
self.player).item_rule = \
|
||||
lambda item: item.classification != ItemClassification.progression
|
||||
lambda item: not item.advancement
|
||||
self.multiworld.get_location("Arnassi Ruins, Arnassi Armor",
|
||||
self.player).item_rule = \
|
||||
lambda item: item.classification != ItemClassification.progression
|
||||
lambda item: not item.advancement
|
||||
|
||||
def adjusting_rules(self, options: AquariaOptions) -> None:
|
||||
"""
|
||||
|
||||
@@ -117,16 +117,13 @@ class AquariaWorld(World):
|
||||
Create an AquariaItem using 'name' as item name.
|
||||
"""
|
||||
result: AquariaItem
|
||||
try:
|
||||
data = item_table[name]
|
||||
classification: ItemClassification = ItemClassification.useful
|
||||
if data.type == ItemType.JUNK:
|
||||
classification = ItemClassification.filler
|
||||
elif data.type == ItemType.PROGRESSION:
|
||||
classification = ItemClassification.progression
|
||||
result = AquariaItem(name, classification, data.id, self.player)
|
||||
except BaseException:
|
||||
raise Exception('The item ' + name + ' is not valid.')
|
||||
data = item_table[name]
|
||||
classification: ItemClassification = ItemClassification.useful
|
||||
if data.type == ItemType.JUNK:
|
||||
classification = ItemClassification.filler
|
||||
elif data.type == ItemType.PROGRESSION:
|
||||
classification = ItemClassification.progression
|
||||
result = AquariaItem(name, classification, data.id, self.player)
|
||||
|
||||
return result
|
||||
|
||||
|
||||
@@ -49,7 +49,7 @@ class UNoProgressionHardHiddenTest(AquariaTestBase):
|
||||
for location in self.unfillable_locations:
|
||||
for item_name in self.world.item_names:
|
||||
item = self.get_item_by_name(item_name)
|
||||
if item.classification == ItemClassification.progression:
|
||||
if item.advancement:
|
||||
self.assertFalse(
|
||||
self.world.get_location(location).can_fill(self.multiworld.state, item, False),
|
||||
"The location \"" + location + "\" can be filled with \"" + item_name + "\"")
|
||||
|
||||
@@ -105,8 +105,8 @@ function on_player_changed_position(event)
|
||||
end
|
||||
local target_direction = exit_table[outbound_direction]
|
||||
|
||||
local target_position = {(CHUNK_OFFSET[target_direction][1] + last_x_chunk) * 32 + 16,
|
||||
(CHUNK_OFFSET[target_direction][2] + last_y_chunk) * 32 + 16}
|
||||
local target_position = {(CHUNK_OFFSET[target_direction][1] + last_x_chunk) * 32 + 16,
|
||||
(CHUNK_OFFSET[target_direction][2] + last_y_chunk) * 32 + 16}
|
||||
target_position = character.surface.find_non_colliding_position(character.prototype.name,
|
||||
target_position, 32, 0.5)
|
||||
if target_position ~= nil then
|
||||
@@ -134,40 +134,96 @@ end
|
||||
|
||||
script.on_event(defines.events.on_player_changed_position, on_player_changed_position)
|
||||
{% endif %}
|
||||
|
||||
function count_energy_bridges()
|
||||
local count = 0
|
||||
for i, bridge in pairs(storage.energy_link_bridges) do
|
||||
if validate_energy_link_bridge(i, bridge) then
|
||||
count = count + 1 + (bridge.quality.level * 0.3)
|
||||
end
|
||||
end
|
||||
return count
|
||||
end
|
||||
function get_energy_increment(bridge)
|
||||
return ENERGY_INCREMENT + (ENERGY_INCREMENT * 0.3 * bridge.quality.level)
|
||||
end
|
||||
function on_check_energy_link(event)
|
||||
--- assuming 1 MJ increment and 5MJ battery:
|
||||
--- first 2 MJ request fill, last 2 MJ push energy, middle 1 MJ does nothing
|
||||
if event.tick % 60 == 30 then
|
||||
local surface = game.get_surface(1)
|
||||
local force = "player"
|
||||
local bridges = surface.find_entities_filtered({name="ap-energy-bridge", force=force})
|
||||
local bridgecount = table_size(bridges)
|
||||
local bridges = storage.energy_link_bridges
|
||||
local bridgecount = count_energy_bridges()
|
||||
storage.forcedata[force].energy_bridges = bridgecount
|
||||
if storage.forcedata[force].energy == nil then
|
||||
storage.forcedata[force].energy = 0
|
||||
end
|
||||
if storage.forcedata[force].energy < ENERGY_INCREMENT * bridgecount * 5 then
|
||||
for i, bridge in ipairs(bridges) do
|
||||
if bridge.energy > ENERGY_INCREMENT*3 then
|
||||
storage.forcedata[force].energy = storage.forcedata[force].energy + (ENERGY_INCREMENT * ENERGY_LINK_EFFICIENCY)
|
||||
bridge.energy = bridge.energy - ENERGY_INCREMENT
|
||||
for i, bridge in pairs(bridges) do
|
||||
if validate_energy_link_bridge(i, bridge) then
|
||||
energy_increment = get_energy_increment(bridge)
|
||||
if bridge.energy > energy_increment*3 then
|
||||
storage.forcedata[force].energy = storage.forcedata[force].energy + (energy_increment * ENERGY_LINK_EFFICIENCY)
|
||||
bridge.energy = bridge.energy - energy_increment
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
for i, bridge in ipairs(bridges) do
|
||||
if storage.forcedata[force].energy < ENERGY_INCREMENT then
|
||||
break
|
||||
end
|
||||
if bridge.energy < ENERGY_INCREMENT*2 and storage.forcedata[force].energy > ENERGY_INCREMENT then
|
||||
storage.forcedata[force].energy = storage.forcedata[force].energy - ENERGY_INCREMENT
|
||||
bridge.energy = bridge.energy + ENERGY_INCREMENT
|
||||
for i, bridge in pairs(bridges) do
|
||||
if validate_energy_link_bridge(i, bridge) then
|
||||
energy_increment = get_energy_increment(bridge)
|
||||
if storage.forcedata[force].energy < energy_increment and bridge.quality.level == 0 then
|
||||
break
|
||||
end
|
||||
if bridge.energy < energy_increment*2 and storage.forcedata[force].energy > energy_increment then
|
||||
storage.forcedata[force].energy = storage.forcedata[force].energy - energy_increment
|
||||
bridge.energy = bridge.energy + energy_increment
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
end
|
||||
function string_starts_with(str, start)
|
||||
return str:sub(1, #start) == start
|
||||
end
|
||||
function validate_energy_link_bridge(unit_number, entity)
|
||||
if not entity then
|
||||
if storage.energy_link_bridges[unit_number] == nil then return false end
|
||||
storage.energy_link_bridges[unit_number] = nil
|
||||
return false
|
||||
end
|
||||
if not entity.valid then
|
||||
if storage.energy_link_bridges[unit_number] == nil then return false end
|
||||
storage.energy_link_bridges[unit_number] = nil
|
||||
return false
|
||||
end
|
||||
return true
|
||||
end
|
||||
function on_energy_bridge_constructed(entity)
|
||||
if entity and entity.valid then
|
||||
if string_starts_with(entity.prototype.name, "ap-energy-bridge") then
|
||||
storage.energy_link_bridges[entity.unit_number] = entity
|
||||
end
|
||||
end
|
||||
end
|
||||
function on_energy_bridge_removed(entity)
|
||||
if string_starts_with(entity.prototype.name, "ap-energy-bridge") then
|
||||
if storage.energy_link_bridges[entity.unit_number] == nil then return end
|
||||
storage.energy_link_bridges[entity.unit_number] = nil
|
||||
end
|
||||
end
|
||||
if (ENERGY_INCREMENT) then
|
||||
script.on_event(defines.events.on_tick, on_check_energy_link)
|
||||
|
||||
script.on_event({defines.events.on_built_entity}, function(event) on_energy_bridge_constructed(event.entity) end)
|
||||
script.on_event({defines.events.on_robot_built_entity}, function(event) on_energy_bridge_constructed(event.entity) end)
|
||||
script.on_event({defines.events.on_entity_cloned}, function(event) on_energy_bridge_constructed(event.destination) end)
|
||||
|
||||
script.on_event({defines.events.script_raised_revive}, function(event) on_energy_bridge_constructed(event.entity) end)
|
||||
script.on_event({defines.events.script_raised_built}, function(event) on_energy_bridge_constructed(event.entity) end)
|
||||
|
||||
script.on_event({defines.events.on_entity_died}, function(event) on_energy_bridge_removed(event.entity) end)
|
||||
script.on_event({defines.events.on_player_mined_entity}, function(event) on_energy_bridge_removed(event.entity) end)
|
||||
script.on_event({defines.events.on_robot_mined_entity}, function(event) on_energy_bridge_removed(event.entity) end)
|
||||
end
|
||||
|
||||
{% if not imported_blueprints -%}
|
||||
@@ -410,6 +466,7 @@ script.on_init(function()
|
||||
{% if not imported_blueprints %}set_permissions(){% endif %}
|
||||
storage.forcedata = {}
|
||||
storage.playerdata = {}
|
||||
storage.energy_link_bridges = {}
|
||||
-- Fire dummy events for all currently existing forces.
|
||||
local e = {}
|
||||
for name, _ in pairs(game.forces) do
|
||||
|
||||
@@ -47,6 +47,17 @@ def get_flag(data, flag):
|
||||
bit = int(0x80 / (2 ** (flag % 8)))
|
||||
return (data[byte] & bit) > 0
|
||||
|
||||
def validate_read_state(data1, data2):
|
||||
validation_array = bytes([0x01, 0x46, 0x46, 0x4D, 0x51, 0x52])
|
||||
|
||||
if data1 is None or data2 is None:
|
||||
return False
|
||||
for i in range(6):
|
||||
if data1[i] != validation_array[i] or data2[i] != validation_array[i]:
|
||||
return False;
|
||||
return True
|
||||
|
||||
|
||||
|
||||
class FFMQClient(SNIClient):
|
||||
game = "Final Fantasy Mystic Quest"
|
||||
@@ -67,11 +78,11 @@ class FFMQClient(SNIClient):
|
||||
async def game_watcher(self, ctx):
|
||||
from SNIClient import snes_buffered_write, snes_flush_writes, snes_read
|
||||
|
||||
check_1 = await snes_read(ctx, 0xF53749, 1)
|
||||
check_1 = await snes_read(ctx, 0xF53749, 6)
|
||||
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'\x01' or check_2 != b'\x01':
|
||||
check_2 = await snes_read(ctx, 0xF53749, 6)
|
||||
if not validate_read_state(check_1, check_2):
|
||||
return
|
||||
|
||||
def get_range(data_range):
|
||||
|
||||
@@ -69,7 +69,7 @@ def locality_rules(multiworld: MultiWorld):
|
||||
if (location.player, location.item_rule) in func_cache:
|
||||
location.item_rule = func_cache[location.player, location.item_rule]
|
||||
# empty rule that just returns True, overwrite
|
||||
elif location.item_rule is location.__class__.item_rule:
|
||||
elif location.item_rule is Location.item_rule:
|
||||
func_cache[location.player, location.item_rule] = location.item_rule = \
|
||||
lambda i, sending_blockers = forbid_data[location.player], \
|
||||
old_rule = location.item_rule: \
|
||||
@@ -103,7 +103,7 @@ def set_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"],
|
||||
def add_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"], rule: CollectionRule, combine="and"):
|
||||
old_rule = spot.access_rule
|
||||
# empty rule, replace instead of add
|
||||
if old_rule is spot.__class__.access_rule:
|
||||
if old_rule is Location.access_rule or old_rule is Entrance.access_rule:
|
||||
spot.access_rule = rule if combine == "and" else old_rule
|
||||
else:
|
||||
if combine == "and":
|
||||
@@ -115,7 +115,7 @@ def add_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"],
|
||||
def forbid_item(location: "BaseClasses.Location", item: str, player: int):
|
||||
old_rule = location.item_rule
|
||||
# empty rule
|
||||
if old_rule is location.__class__.item_rule:
|
||||
if old_rule is Location.item_rule:
|
||||
location.item_rule = lambda i: i.name != item or i.player != player
|
||||
else:
|
||||
location.item_rule = lambda i: (i.name != item or i.player != player) and old_rule(i)
|
||||
@@ -135,7 +135,7 @@ def forbid_items(location: "BaseClasses.Location", items: typing.Set[str]):
|
||||
def add_item_rule(location: "BaseClasses.Location", rule: ItemRule, combine: str = "and"):
|
||||
old_rule = location.item_rule
|
||||
# empty rule, replace instead of add
|
||||
if old_rule is location.__class__.item_rule:
|
||||
if old_rule is Location.item_rule:
|
||||
location.item_rule = rule if combine == "and" else old_rule
|
||||
else:
|
||||
if combine == "and":
|
||||
|
||||
@@ -9,11 +9,7 @@ import ast
|
||||
|
||||
import jinja2
|
||||
|
||||
try:
|
||||
from ast import unparse
|
||||
except ImportError:
|
||||
# Py 3.8 and earlier compatibility module
|
||||
from astunparse import unparse
|
||||
from ast import unparse
|
||||
|
||||
from Utils import get_text_between
|
||||
|
||||
|
||||
@@ -231,7 +231,7 @@ class HKWorld(World):
|
||||
all_event_names.update(set(godhome_event_names))
|
||||
|
||||
# Link regions
|
||||
for event_name in all_event_names:
|
||||
for event_name in sorted(all_event_names):
|
||||
#if event_name in wp_exclusions:
|
||||
# continue
|
||||
loc = HKLocation(self.player, event_name, None, menu_region)
|
||||
|
||||
@@ -1 +0,0 @@
|
||||
astunparse>=1.6.3; python_version <= '3.8'
|
||||
@@ -235,6 +235,11 @@ def set_rules(kh1world):
|
||||
lambda state: (
|
||||
state.has("Progressive Glide", player)
|
||||
or
|
||||
(
|
||||
state.has("High Jump", player, 2)
|
||||
and state.has("Footprints", player)
|
||||
)
|
||||
or
|
||||
(
|
||||
options.advanced_logic
|
||||
and state.has_all({
|
||||
@@ -246,6 +251,11 @@ def set_rules(kh1world):
|
||||
lambda state: (
|
||||
state.has("Progressive Glide", player)
|
||||
or
|
||||
(
|
||||
state.has("High Jump", player, 2)
|
||||
and state.has("Footprints", player)
|
||||
)
|
||||
or
|
||||
(
|
||||
options.advanced_logic
|
||||
and state.has_all({
|
||||
@@ -258,7 +268,6 @@ def set_rules(kh1world):
|
||||
|
||||
state.has("Footprints", player)
|
||||
or (options.advanced_logic and state.has("Progressive Glide", player))
|
||||
or state.has("High Jump", player, 2)
|
||||
))
|
||||
add_rule(kh1world.get_location("Wonderland Tea Party Garden Across From Bizarre Room Entrance Chest"),
|
||||
lambda state: (
|
||||
@@ -376,7 +385,7 @@ def set_rules(kh1world):
|
||||
lambda state: state.has("White Trinity", player))
|
||||
add_rule(kh1world.get_location("Monstro Chamber 6 Other Platform Chest"),
|
||||
lambda state: (
|
||||
state.has("High Jump", player)
|
||||
state.has_all(("High Jump", "Progressive Glide"), player)
|
||||
or (options.advanced_logic and state.has("Combo Master", player))
|
||||
))
|
||||
add_rule(kh1world.get_location("Monstro Chamber 6 Platform Near Chamber 5 Entrance Chest"),
|
||||
@@ -386,7 +395,7 @@ def set_rules(kh1world):
|
||||
))
|
||||
add_rule(kh1world.get_location("Monstro Chamber 6 Raised Area Near Chamber 1 Entrance Chest"),
|
||||
lambda state: (
|
||||
state.has("High Jump", player)
|
||||
state.has_all(("High Jump", "Progressive Glide"), player)
|
||||
or (options.advanced_logic and state.has("Combo Master", player))
|
||||
))
|
||||
add_rule(kh1world.get_location("Halloween Town Moonlight Hill White Trinity Chest"),
|
||||
@@ -595,6 +604,7 @@ def set_rules(kh1world):
|
||||
lambda state: (
|
||||
state.has("Green Trinity", player)
|
||||
and has_all_magic_lvx(state, player, 2)
|
||||
and has_defensive_tools(state, player)
|
||||
))
|
||||
add_rule(kh1world.get_location("Neverland Hold Flight 2nd Chest"),
|
||||
lambda state: (
|
||||
@@ -710,8 +720,7 @@ def set_rules(kh1world):
|
||||
lambda state: state.has("White Trinity", player))
|
||||
add_rule(kh1world.get_location("End of the World Giant Crevasse 5th Chest"),
|
||||
lambda state: (
|
||||
state.has("High Jump", player)
|
||||
or state.has("Progressive Glide", player)
|
||||
state.has("Progressive Glide", player)
|
||||
))
|
||||
add_rule(kh1world.get_location("End of the World Giant Crevasse 1st Chest"),
|
||||
lambda state: (
|
||||
@@ -1441,10 +1450,11 @@ def set_rules(kh1world):
|
||||
has_emblems(state, player, options.keyblades_unlock_chests)
|
||||
and has_x_worlds(state, player, 7, options.keyblades_unlock_chests)
|
||||
and has_defensive_tools(state, player)
|
||||
and state.has("Progressive Blizzard", player, 3)
|
||||
))
|
||||
add_rule(kh1world.get_location("Agrabah Defeat Kurt Zisa Zantetsuken Event"),
|
||||
lambda state: (
|
||||
has_emblems(state, player, options.keyblades_unlock_chests) and has_x_worlds(state, player, 7, options.keyblades_unlock_chests) and has_defensive_tools(state, player)
|
||||
has_emblems(state, player, options.keyblades_unlock_chests) and has_x_worlds(state, player, 7, options.keyblades_unlock_chests) and has_defensive_tools(state, player) and state.has("Progressive Blizzard", player, 3)
|
||||
))
|
||||
if options.super_bosses or options.goal.current_key == "sephiroth":
|
||||
add_rule(kh1world.get_location("Olympus Coliseum Defeat Sephiroth Ansem's Report 12"),
|
||||
|
||||
@@ -34,7 +34,7 @@ def create_locations(player: int, regions_table: Dict[str, LandstalkerRegion], n
|
||||
for data in WORLD_PATHS_JSON:
|
||||
if "requiredNodes" in data:
|
||||
regions_with_entrance_checks.extend([region_id for region_id in data["requiredNodes"]])
|
||||
regions_with_entrance_checks = list(set(regions_with_entrance_checks))
|
||||
regions_with_entrance_checks = sorted(set(regions_with_entrance_checks))
|
||||
for region_id in regions_with_entrance_checks:
|
||||
region = regions_table[region_id]
|
||||
location = LandstalkerLocation(player, 'event_visited_' + region_id, None, region, "event")
|
||||
|
||||
@@ -118,7 +118,7 @@ class L2ACWorld(World):
|
||||
L2ACItem("Progressive chest access", ItemClassification.progression, None, self.player))
|
||||
chest_access.show_in_spoiler = False
|
||||
ancient_dungeon.locations.append(chest_access)
|
||||
for iris in self.item_name_groups["Iris treasures"]:
|
||||
for iris in sorted(self.item_name_groups["Iris treasures"]):
|
||||
treasure_name: str = f"Iris treasure {self.item_name_to_id[iris] - self.item_name_to_id['Iris sword'] + 1}"
|
||||
iris_treasure: Location = \
|
||||
L2ACLocation(self.player, treasure_name, self.location_name_to_id[treasure_name], ancient_dungeon)
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import logging
|
||||
from typing import Any, ClassVar, Dict, List, Optional, Set, TextIO
|
||||
from typing import Any, ClassVar, TextIO
|
||||
|
||||
from BaseClasses import CollectionState, Entrance, Item, ItemClassification, MultiWorld, Tutorial
|
||||
from Options import Accessibility
|
||||
@@ -120,16 +120,16 @@ class MessengerWorld(World):
|
||||
required_seals: int = 0
|
||||
created_seals: int = 0
|
||||
total_shards: int = 0
|
||||
shop_prices: Dict[str, int]
|
||||
figurine_prices: Dict[str, int]
|
||||
_filler_items: List[str]
|
||||
starting_portals: List[str]
|
||||
plando_portals: List[str]
|
||||
spoiler_portal_mapping: Dict[str, str]
|
||||
portal_mapping: List[int]
|
||||
transitions: List[Entrance]
|
||||
shop_prices: dict[str, int]
|
||||
figurine_prices: dict[str, int]
|
||||
_filler_items: list[str]
|
||||
starting_portals: list[str]
|
||||
plando_portals: list[str]
|
||||
spoiler_portal_mapping: dict[str, str]
|
||||
portal_mapping: list[int]
|
||||
transitions: list[Entrance]
|
||||
reachable_locs: int = 0
|
||||
filler: Dict[str, int]
|
||||
filler: dict[str, int]
|
||||
|
||||
def generate_early(self) -> None:
|
||||
if self.options.goal == Goal.option_power_seal_hunt:
|
||||
@@ -178,7 +178,7 @@ class MessengerWorld(World):
|
||||
for reg_name in sub_region]
|
||||
|
||||
for region in complex_regions:
|
||||
region_name = region.name.replace(f"{region.parent} - ", "")
|
||||
region_name = region.name.removeprefix(f"{region.parent} - ")
|
||||
connection_data = CONNECTIONS[region.parent][region_name]
|
||||
for exit_region in connection_data:
|
||||
region.connect(self.multiworld.get_region(exit_region, self.player))
|
||||
@@ -191,7 +191,7 @@ class MessengerWorld(World):
|
||||
# create items that are always in the item pool
|
||||
main_movement_items = ["Rope Dart", "Wingsuit"]
|
||||
precollected_names = [item.name for item in self.multiworld.precollected_items[self.player]]
|
||||
itempool: List[MessengerItem] = [
|
||||
itempool: list[MessengerItem] = [
|
||||
self.create_item(item)
|
||||
for item in self.item_name_to_id
|
||||
if item not in {
|
||||
@@ -290,7 +290,7 @@ class MessengerWorld(World):
|
||||
for portal, output in portal_info:
|
||||
spoiler.set_entrance(f"{portal} Portal", output, "I can write anything I want here lmao", self.player)
|
||||
|
||||
def fill_slot_data(self) -> Dict[str, Any]:
|
||||
def fill_slot_data(self) -> dict[str, Any]:
|
||||
slot_data = {
|
||||
"shop": {SHOP_ITEMS[item].internal_name: price for item, price in self.shop_prices.items()},
|
||||
"figures": {FIGURINES[item].internal_name: price for item, price in self.figurine_prices.items()},
|
||||
@@ -316,7 +316,7 @@ class MessengerWorld(World):
|
||||
return self._filler_items.pop(0)
|
||||
|
||||
def create_item(self, name: str) -> MessengerItem:
|
||||
item_id: Optional[int] = self.item_name_to_id.get(name, None)
|
||||
item_id: int | None = self.item_name_to_id.get(name, None)
|
||||
return MessengerItem(
|
||||
name,
|
||||
ItemClassification.progression if item_id is None else self.get_item_classification(name),
|
||||
@@ -351,7 +351,7 @@ class MessengerWorld(World):
|
||||
return ItemClassification.filler
|
||||
|
||||
@classmethod
|
||||
def create_group(cls, multiworld: "MultiWorld", new_player_id: int, players: Set[int]) -> World:
|
||||
def create_group(cls, multiworld: "MultiWorld", new_player_id: int, players: set[int]) -> World:
|
||||
group = super().create_group(multiworld, new_player_id, players)
|
||||
assert isinstance(group, MessengerWorld)
|
||||
|
||||
|
||||
@@ -5,7 +5,7 @@ import os.path
|
||||
import subprocess
|
||||
import urllib.request
|
||||
from shutil import which
|
||||
from typing import Any, Optional
|
||||
from typing import Any
|
||||
from zipfile import ZipFile
|
||||
from Utils import open_file
|
||||
|
||||
@@ -17,7 +17,7 @@ from Utils import is_windows, messagebox, tuplize_version
|
||||
MOD_URL = "https://api.github.com/repos/alwaysintreble/TheMessengerRandomizerModAP/releases/latest"
|
||||
|
||||
|
||||
def ask_yes_no_cancel(title: str, text: str) -> Optional[bool]:
|
||||
def ask_yes_no_cancel(title: str, text: str) -> bool | None:
|
||||
"""
|
||||
Wrapper for tkinter.messagebox.askyesnocancel, that creates a popup dialog box with yes, no, and cancel buttons.
|
||||
|
||||
@@ -33,7 +33,6 @@ def ask_yes_no_cancel(title: str, text: str) -> Optional[bool]:
|
||||
return ret
|
||||
|
||||
|
||||
|
||||
def launch_game(*args) -> None:
|
||||
"""Check the game installation, then launch it"""
|
||||
def courier_installed() -> bool:
|
||||
|
||||
@@ -1,6 +1,4 @@
|
||||
from typing import Dict, List
|
||||
|
||||
CONNECTIONS: Dict[str, Dict[str, List[str]]] = {
|
||||
CONNECTIONS: dict[str, dict[str, list[str]]] = {
|
||||
"Ninja Village": {
|
||||
"Right": [
|
||||
"Autumn Hills - Left",
|
||||
@@ -640,7 +638,7 @@ CONNECTIONS: Dict[str, Dict[str, List[str]]] = {
|
||||
},
|
||||
}
|
||||
|
||||
RANDOMIZED_CONNECTIONS: Dict[str, str] = {
|
||||
RANDOMIZED_CONNECTIONS: dict[str, str] = {
|
||||
"Ninja Village - Right": "Autumn Hills - Left",
|
||||
"Autumn Hills - Left": "Ninja Village - Right",
|
||||
"Autumn Hills - Right": "Forlorn Temple - Left",
|
||||
@@ -680,7 +678,7 @@ RANDOMIZED_CONNECTIONS: Dict[str, str] = {
|
||||
"Sunken Shrine - Left": "Howling Grotto - Bottom",
|
||||
}
|
||||
|
||||
TRANSITIONS: List[str] = [
|
||||
TRANSITIONS: list[str] = [
|
||||
"Ninja Village - Right",
|
||||
"Autumn Hills - Left",
|
||||
"Autumn Hills - Right",
|
||||
|
||||
@@ -2,7 +2,7 @@ from .shop import FIGURINES, SHOP_ITEMS
|
||||
|
||||
# items
|
||||
# listing individual groups first for easy lookup
|
||||
NOTES = [
|
||||
NOTES: list[str] = [
|
||||
"Key of Hope",
|
||||
"Key of Chaos",
|
||||
"Key of Courage",
|
||||
@@ -11,7 +11,7 @@ NOTES = [
|
||||
"Key of Symbiosis",
|
||||
]
|
||||
|
||||
PROG_ITEMS = [
|
||||
PROG_ITEMS: list[str] = [
|
||||
"Wingsuit",
|
||||
"Rope Dart",
|
||||
"Lightfoot Tabi",
|
||||
@@ -28,18 +28,18 @@ PROG_ITEMS = [
|
||||
"Seashell",
|
||||
]
|
||||
|
||||
PHOBEKINS = [
|
||||
PHOBEKINS: list[str] = [
|
||||
"Necro",
|
||||
"Pyro",
|
||||
"Claustro",
|
||||
"Acro",
|
||||
]
|
||||
|
||||
USEFUL_ITEMS = [
|
||||
USEFUL_ITEMS: list[str] = [
|
||||
"Windmill Shuriken",
|
||||
]
|
||||
|
||||
FILLER = {
|
||||
FILLER: dict[str, int] = {
|
||||
"Time Shard": 5,
|
||||
"Time Shard (10)": 10,
|
||||
"Time Shard (50)": 20,
|
||||
@@ -48,13 +48,13 @@ FILLER = {
|
||||
"Time Shard (500)": 5,
|
||||
}
|
||||
|
||||
TRAPS = {
|
||||
TRAPS: dict[str, int] = {
|
||||
"Teleport Trap": 5,
|
||||
"Prophecy Trap": 10,
|
||||
}
|
||||
|
||||
# item_name_to_id needs to be deterministic and match upstream
|
||||
ALL_ITEMS = [
|
||||
ALL_ITEMS: list[str] = [
|
||||
*NOTES,
|
||||
"Windmill Shuriken",
|
||||
"Wingsuit",
|
||||
@@ -83,7 +83,7 @@ ALL_ITEMS = [
|
||||
# locations
|
||||
# the names of these don't actually matter, but using the upstream's names for now
|
||||
# order must be exactly the same as upstream
|
||||
ALWAYS_LOCATIONS = [
|
||||
ALWAYS_LOCATIONS: list[str] = [
|
||||
# notes
|
||||
"Sunken Shrine - Key of Love",
|
||||
"Corrupted Future - Key of Courage",
|
||||
@@ -160,7 +160,7 @@ ALWAYS_LOCATIONS = [
|
||||
"Elemental Skylands Seal - Fire",
|
||||
]
|
||||
|
||||
BOSS_LOCATIONS = [
|
||||
BOSS_LOCATIONS: list[str] = [
|
||||
"Autumn Hills - Leaf Golem",
|
||||
"Catacombs - Ruxxtin",
|
||||
"Howling Grotto - Emerald Golem",
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Dict
|
||||
|
||||
from schema import And, Optional, Or, Schema
|
||||
|
||||
@@ -167,7 +166,7 @@ class ShopPrices(Range):
|
||||
default = 100
|
||||
|
||||
|
||||
def planned_price(location: str) -> Dict[Optional, Or]:
|
||||
def planned_price(location: str) -> dict[Optional, Or]:
|
||||
return {
|
||||
Optional(location): Or(
|
||||
And(int, lambda n: n >= 0),
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from copy import deepcopy
|
||||
from typing import List, TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from BaseClasses import CollectionState, PlandoOptions
|
||||
from Options import PlandoConnection
|
||||
@@ -8,7 +8,7 @@ if TYPE_CHECKING:
|
||||
from . import MessengerWorld
|
||||
|
||||
|
||||
PORTALS = [
|
||||
PORTALS: list[str] = [
|
||||
"Autumn Hills",
|
||||
"Riviere Turquoise",
|
||||
"Howling Grotto",
|
||||
@@ -18,7 +18,7 @@ PORTALS = [
|
||||
]
|
||||
|
||||
|
||||
SHOP_POINTS = {
|
||||
SHOP_POINTS: dict[str, list[str]] = {
|
||||
"Autumn Hills": [
|
||||
"Climbing Claws",
|
||||
"Hope Path",
|
||||
@@ -113,7 +113,7 @@ SHOP_POINTS = {
|
||||
}
|
||||
|
||||
|
||||
CHECKPOINTS = {
|
||||
CHECKPOINTS: dict[str, list[str]] = {
|
||||
"Autumn Hills": [
|
||||
"Hope Latch",
|
||||
"Key of Hope",
|
||||
@@ -186,7 +186,7 @@ CHECKPOINTS = {
|
||||
}
|
||||
|
||||
|
||||
REGION_ORDER = [
|
||||
REGION_ORDER: list[str] = [
|
||||
"Autumn Hills",
|
||||
"Forlorn Temple",
|
||||
"Catacombs",
|
||||
@@ -228,7 +228,7 @@ def shuffle_portals(world: "MessengerWorld") -> None:
|
||||
|
||||
return parent
|
||||
|
||||
def handle_planned_portals(plando_connections: List[PlandoConnection]) -> None:
|
||||
def handle_planned_portals(plando_connections: list[PlandoConnection]) -> None:
|
||||
"""checks the provided plando connections for portals and connects them"""
|
||||
nonlocal available_portals
|
||||
|
||||
|
||||
@@ -1,7 +1,4 @@
|
||||
from typing import Dict, List
|
||||
|
||||
|
||||
LOCATIONS: Dict[str, List[str]] = {
|
||||
LOCATIONS: dict[str, list[str]] = {
|
||||
"Ninja Village - Nest": [
|
||||
"Ninja Village - Candle",
|
||||
"Ninja Village - Astral Seed",
|
||||
@@ -201,7 +198,7 @@ LOCATIONS: Dict[str, List[str]] = {
|
||||
}
|
||||
|
||||
|
||||
SUB_REGIONS: Dict[str, List[str]] = {
|
||||
SUB_REGIONS: dict[str, list[str]] = {
|
||||
"Ninja Village": [
|
||||
"Right",
|
||||
],
|
||||
@@ -385,7 +382,7 @@ SUB_REGIONS: Dict[str, List[str]] = {
|
||||
|
||||
|
||||
# order is slightly funky here for back compat
|
||||
MEGA_SHARDS: Dict[str, List[str]] = {
|
||||
MEGA_SHARDS: dict[str, list[str]] = {
|
||||
"Autumn Hills - Lakeside Checkpoint": ["Autumn Hills Mega Shard"],
|
||||
"Forlorn Temple - Outside Shop": ["Hidden Entrance Mega Shard"],
|
||||
"Catacombs - Top Left": ["Catacombs Mega Shard"],
|
||||
@@ -414,7 +411,7 @@ MEGA_SHARDS: Dict[str, List[str]] = {
|
||||
}
|
||||
|
||||
|
||||
REGION_CONNECTIONS: Dict[str, Dict[str, str]] = {
|
||||
REGION_CONNECTIONS: dict[str, dict[str, str]] = {
|
||||
"Menu": {"Tower HQ": "Start Game"},
|
||||
"Tower HQ": {
|
||||
"Autumn Hills - Portal": "ToTHQ Autumn Hills Portal",
|
||||
@@ -436,7 +433,7 @@ REGION_CONNECTIONS: Dict[str, Dict[str, str]] = {
|
||||
|
||||
|
||||
# regions that don't have sub-regions
|
||||
LEVELS: List[str] = [
|
||||
LEVELS: list[str] = [
|
||||
"Menu",
|
||||
"Tower HQ",
|
||||
"The Shop",
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
from typing import Dict, TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from BaseClasses import CollectionState
|
||||
from worlds.generic.Rules import CollectionRule, add_rule, allow_self_locking_items
|
||||
@@ -12,9 +12,9 @@ if TYPE_CHECKING:
|
||||
class MessengerRules:
|
||||
player: int
|
||||
world: "MessengerWorld"
|
||||
connection_rules: Dict[str, CollectionRule]
|
||||
region_rules: Dict[str, CollectionRule]
|
||||
location_rules: Dict[str, CollectionRule]
|
||||
connection_rules: dict[str, CollectionRule]
|
||||
region_rules: dict[str, CollectionRule]
|
||||
location_rules: dict[str, CollectionRule]
|
||||
maximum_price: int
|
||||
required_seals: int
|
||||
|
||||
|
||||
@@ -1,11 +1,11 @@
|
||||
from typing import Dict, List, NamedTuple, Optional, Set, TYPE_CHECKING, Tuple, Union
|
||||
from typing import NamedTuple, TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import MessengerWorld
|
||||
else:
|
||||
MessengerWorld = object
|
||||
|
||||
PROG_SHOP_ITEMS: List[str] = [
|
||||
PROG_SHOP_ITEMS: list[str] = [
|
||||
"Path of Resilience",
|
||||
"Meditation",
|
||||
"Strike of the Ninja",
|
||||
@@ -14,7 +14,7 @@ PROG_SHOP_ITEMS: List[str] = [
|
||||
"Aerobatics Warrior",
|
||||
]
|
||||
|
||||
USEFUL_SHOP_ITEMS: List[str] = [
|
||||
USEFUL_SHOP_ITEMS: list[str] = [
|
||||
"Karuta Plates",
|
||||
"Serendipitous Bodies",
|
||||
"Kusari Jacket",
|
||||
@@ -29,10 +29,10 @@ class ShopData(NamedTuple):
|
||||
internal_name: str
|
||||
min_price: int
|
||||
max_price: int
|
||||
prerequisite: Optional[Union[str, Set[str]]] = None
|
||||
prerequisite: str | set[str] | None = None
|
||||
|
||||
|
||||
SHOP_ITEMS: Dict[str, ShopData] = {
|
||||
SHOP_ITEMS: dict[str, ShopData] = {
|
||||
"Karuta Plates": ShopData("HP_UPGRADE_1", 20, 200),
|
||||
"Serendipitous Bodies": ShopData("ENEMY_DROP_HP", 20, 300, "The Shop - Karuta Plates"),
|
||||
"Path of Resilience": ShopData("DAMAGE_REDUCTION", 100, 500, "The Shop - Serendipitous Bodies"),
|
||||
@@ -56,7 +56,7 @@ SHOP_ITEMS: Dict[str, ShopData] = {
|
||||
"Focused Power Sense": ShopData("POWER_SEAL_WORLD_MAP", 300, 600, "The Shop - Power Sense"),
|
||||
}
|
||||
|
||||
FIGURINES: Dict[str, ShopData] = {
|
||||
FIGURINES: dict[str, ShopData] = {
|
||||
"Green Kappa Figurine": ShopData("GREEN_KAPPA", 100, 500),
|
||||
"Blue Kappa Figurine": ShopData("BLUE_KAPPA", 100, 500),
|
||||
"Ountarde Figurine": ShopData("OUNTARDE", 100, 500),
|
||||
@@ -73,12 +73,12 @@ FIGURINES: Dict[str, ShopData] = {
|
||||
}
|
||||
|
||||
|
||||
def shuffle_shop_prices(world: MessengerWorld) -> Tuple[Dict[str, int], Dict[str, int]]:
|
||||
def shuffle_shop_prices(world: MessengerWorld) -> tuple[dict[str, int], dict[str, int]]:
|
||||
shop_price_mod = world.options.shop_price.value
|
||||
shop_price_planned = world.options.shop_price_plan
|
||||
|
||||
shop_prices: Dict[str, int] = {}
|
||||
figurine_prices: Dict[str, int] = {}
|
||||
shop_prices: dict[str, int] = {}
|
||||
figurine_prices: dict[str, int] = {}
|
||||
for item, price in shop_price_planned.value.items():
|
||||
if not isinstance(price, int):
|
||||
price = world.random.choices(list(price.keys()), weights=list(price.values()))[0]
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from functools import cached_property
|
||||
from typing import Optional, TYPE_CHECKING
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from BaseClasses import CollectionState, Entrance, Item, ItemClassification, Location, Region
|
||||
from .regions import LOCATIONS, MEGA_SHARDS
|
||||
@@ -10,14 +10,14 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
class MessengerEntrance(Entrance):
|
||||
world: Optional["MessengerWorld"] = None
|
||||
world: "MessengerWorld | None" = None
|
||||
|
||||
|
||||
class MessengerRegion(Region):
|
||||
parent: str
|
||||
entrance_type = MessengerEntrance
|
||||
|
||||
def __init__(self, name: str, world: "MessengerWorld", parent: Optional[str] = None) -> None:
|
||||
def __init__(self, name: str, world: "MessengerWorld", parent: str | None = None) -> None:
|
||||
super().__init__(name, world.player, world.multiworld)
|
||||
self.parent = parent
|
||||
locations = []
|
||||
@@ -48,7 +48,7 @@ class MessengerRegion(Region):
|
||||
class MessengerLocation(Location):
|
||||
game = "The Messenger"
|
||||
|
||||
def __init__(self, player: int, name: str, loc_id: Optional[int], parent: MessengerRegion) -> None:
|
||||
def __init__(self, player: int, name: str, loc_id: int | None, parent: MessengerRegion) -> None:
|
||||
super().__init__(player, name, loc_id, parent)
|
||||
if loc_id is None:
|
||||
if name == "Rescue Phantom":
|
||||
@@ -59,7 +59,7 @@ class MessengerLocation(Location):
|
||||
class MessengerShopLocation(MessengerLocation):
|
||||
@cached_property
|
||||
def cost(self) -> int:
|
||||
name = self.name.replace("The Shop - ", "") # TODO use `remove_prefix` when 3.8 finally gets dropped
|
||||
name = self.name.removeprefix("The Shop - ")
|
||||
world = self.parent_region.multiworld.worlds[self.player]
|
||||
shop_data = SHOP_ITEMS[name]
|
||||
if shop_data.prerequisite:
|
||||
|
||||
@@ -77,7 +77,7 @@ class PlandoTest(MessengerTestBase):
|
||||
|
||||
loc = f"The Shop - {loc}"
|
||||
self.assertLessEqual(price, self.multiworld.get_location(loc, self.player).cost)
|
||||
self.assertTrue(loc.replace("The Shop - ", "") in SHOP_ITEMS)
|
||||
self.assertTrue(loc.removeprefix("The Shop - ") in SHOP_ITEMS)
|
||||
self.assertEqual(len(prices), len(SHOP_ITEMS))
|
||||
|
||||
figures = self.world.figurine_prices
|
||||
|
||||
@@ -96,13 +96,13 @@ class MM2World(World):
|
||||
location_name_groups = location_groups
|
||||
web = MM2WebWorld()
|
||||
rom_name: bytearray
|
||||
world_version: Tuple[int, int, int] = (0, 3, 1)
|
||||
world_version: Tuple[int, int, int] = (0, 3, 2)
|
||||
wily_5_weapons: Dict[int, List[int]]
|
||||
|
||||
def __init__(self, world: MultiWorld, player: int):
|
||||
def __init__(self, multiworld: MultiWorld, player: int):
|
||||
self.rom_name = bytearray()
|
||||
self.rom_name_available_event = threading.Event()
|
||||
super().__init__(world, player)
|
||||
super().__init__(multiworld, player)
|
||||
self.weapon_damage = deepcopy(weapon_damage)
|
||||
self.wily_5_weapons = {}
|
||||
|
||||
|
||||
@@ -133,28 +133,6 @@ def set_rules(world: "MM2World") -> None:
|
||||
# Wily Machine needs all three weaknesses present, so allow
|
||||
elif 4 > world.weapon_damage[weapon][i] > 0:
|
||||
world.weapon_damage[weapon][i] = 0
|
||||
# handle special cases
|
||||
for boss in range(14):
|
||||
for weapon in (1, 3, 6, 8):
|
||||
if (0 < world.weapon_damage[weapon][boss] < minimum_weakness_requirement[weapon] and
|
||||
not any(world.weapon_damage[i][boss] > 0 for i in range(1, 8) if i != weapon)):
|
||||
# Weapon does not have enough possible ammo to kill the boss, raise the damage
|
||||
if boss == 9:
|
||||
if weapon != 3:
|
||||
# Atomic Fire and Crash Bomber cannot be Picopico-kun's only weakness
|
||||
world.weapon_damage[weapon][boss] = 0
|
||||
weakness = world.random.choice((2, 3, 4, 5, 7, 8))
|
||||
world.weapon_damage[weakness][boss] = minimum_weakness_requirement[weakness]
|
||||
elif boss == 11:
|
||||
if weapon == 1:
|
||||
# Atomic Fire cannot be Boobeam Trap's only weakness
|
||||
world.weapon_damage[weapon][boss] = 0
|
||||
weakness = world.random.choice((2, 3, 4, 5, 6, 7, 8))
|
||||
world.weapon_damage[weakness][boss] = minimum_weakness_requirement[weakness]
|
||||
else:
|
||||
world.weapon_damage[weapon][boss] = minimum_weakness_requirement[weapon]
|
||||
starting = world.options.starting_robot_master.value
|
||||
world.weapon_damage[0][starting] = 1
|
||||
|
||||
for p_boss in world.options.plando_weakness:
|
||||
for p_weapon in world.options.plando_weakness[p_boss]:
|
||||
@@ -168,6 +146,28 @@ def set_rules(world: "MM2World") -> None:
|
||||
world.weapon_damage[weapons_to_id[p_weapon]][bosses[p_boss]] \
|
||||
= world.options.plando_weakness[p_boss][p_weapon]
|
||||
|
||||
# handle special cases
|
||||
for boss in range(14):
|
||||
for weapon in (1, 2, 3, 6, 8):
|
||||
if (0 < world.weapon_damage[weapon][boss] < minimum_weakness_requirement[weapon] and
|
||||
not any(world.weapon_damage[i][boss] >= minimum_weakness_requirement[weapon]
|
||||
for i in range(9) if i != weapon)):
|
||||
# Weapon does not have enough possible ammo to kill the boss, raise the damage
|
||||
if boss == 9:
|
||||
if weapon in (1, 6):
|
||||
# Atomic Fire and Crash Bomber cannot be Picopico-kun's only weakness
|
||||
world.weapon_damage[weapon][boss] = 0
|
||||
weakness = world.random.choice((2, 3, 4, 5, 7, 8))
|
||||
world.weapon_damage[weakness][boss] = minimum_weakness_requirement[weakness]
|
||||
elif boss == 11:
|
||||
if weapon == 1:
|
||||
# Atomic Fire cannot be Boobeam Trap's only weakness
|
||||
world.weapon_damage[weapon][boss] = 0
|
||||
weakness = world.random.choice((2, 3, 4, 5, 6, 7, 8))
|
||||
world.weapon_damage[weakness][boss] = minimum_weakness_requirement[weakness]
|
||||
else:
|
||||
world.weapon_damage[weapon][boss] = minimum_weakness_requirement[weapon]
|
||||
|
||||
if world.weapon_damage[0][world.options.starting_robot_master.value] < 1:
|
||||
world.weapon_damage[0][world.options.starting_robot_master.value] = weapon_damage[0][world.options.starting_robot_master.value]
|
||||
|
||||
@@ -209,11 +209,11 @@ def set_rules(world: "MM2World") -> None:
|
||||
continue
|
||||
highest, wp = max(zip(weapon_weight.values(), weapon_weight.keys()))
|
||||
uses = weapon_energy[wp] // weapon_costs[wp]
|
||||
used_weapons[boss].add(wp)
|
||||
if int(uses * boss_damage[wp]) > boss_health[boss]:
|
||||
used = ceil(boss_health[boss] / boss_damage[wp])
|
||||
weapon_energy[wp] -= weapon_costs[wp] * used
|
||||
boss_health[boss] = 0
|
||||
used_weapons[boss].add(wp)
|
||||
elif highest <= 0:
|
||||
# we are out of weapons that can actually damage the boss
|
||||
# so find the weapon that has the most uses, and apply that as an additional weakness
|
||||
@@ -221,18 +221,21 @@ def set_rules(world: "MM2World") -> None:
|
||||
# Quick Boomerang and no other, it would only be 28 off from defeating all 9, which Metal Blade should
|
||||
# be able to cover
|
||||
wp, max_uses = max((weapon, weapon_energy[weapon] // weapon_costs[weapon]) for weapon in weapon_weight
|
||||
if weapon != 0)
|
||||
if weapon != 0 and (weapon != 8 or boss != 12))
|
||||
# Wily Machine cannot under any circumstances take damage from Time Stopper, prevent this
|
||||
world.weapon_damage[wp][boss] = minimum_weakness_requirement[wp]
|
||||
used = min(int(weapon_energy[wp] // weapon_costs[wp]),
|
||||
ceil(boss_health[boss] // minimum_weakness_requirement[wp]))
|
||||
ceil(boss_health[boss] / minimum_weakness_requirement[wp]))
|
||||
weapon_energy[wp] -= weapon_costs[wp] * used
|
||||
boss_health[boss] -= int(used * minimum_weakness_requirement[wp])
|
||||
weapon_weight.pop(wp)
|
||||
used_weapons[boss].add(wp)
|
||||
else:
|
||||
# drain the weapon and continue
|
||||
boss_health[boss] -= int(uses * boss_damage[wp])
|
||||
weapon_energy[wp] -= weapon_costs[wp] * uses
|
||||
weapon_weight.pop(wp)
|
||||
used_weapons[boss].add(wp)
|
||||
|
||||
world.wily_5_weapons = {boss: sorted(used_weapons[boss]) for boss in used_weapons}
|
||||
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from typing import DefaultDict
|
||||
from collections import defaultdict
|
||||
|
||||
MM2_WEAPON_ENCODING: DefaultDict[str, int] = defaultdict(lambda x: 0x6F, {
|
||||
MM2_WEAPON_ENCODING: DefaultDict[str, int] = defaultdict(lambda: 0x6F, {
|
||||
' ': 0x40,
|
||||
'A': 0x41,
|
||||
'B': 0x42,
|
||||
|
||||
@@ -57,11 +57,11 @@ location_rows = [
|
||||
LocationRow('Catch a Swordfish', 'fishing', ['Lobster Spot', ], [SkillRequirement('Fishing', 50), ], [], 12),
|
||||
LocationRow('Bake a Redberry Pie', 'cooking', ['Redberry Bush', 'Wheat', 'Windmill', 'Pie Dish', ], [SkillRequirement('Cooking', 10), ], [], 0),
|
||||
LocationRow('Cook some Stew', 'cooking', ['Bowl', 'Meat', 'Potato', ], [SkillRequirement('Cooking', 25), ], [], 0),
|
||||
LocationRow('Bake an Apple Pie', 'cooking', ['Cooking Apple', 'Wheat', 'Windmill', 'Pie Dish', ], [SkillRequirement('Cooking', 30), ], [], 2),
|
||||
LocationRow('Bake an Apple Pie', 'cooking', ['Cooking Apple', 'Wheat', 'Windmill', 'Pie Dish', ], [SkillRequirement('Cooking', 32), ], [], 2),
|
||||
LocationRow('Bake a Cake', 'cooking', ['Wheat', 'Windmill', 'Egg', 'Milk', 'Cake Tin', ], [SkillRequirement('Cooking', 40), ], [], 6),
|
||||
LocationRow('Bake a Meat Pizza', 'cooking', ['Wheat', 'Windmill', 'Cheese', 'Tomato', 'Meat', ], [SkillRequirement('Cooking', 45), ], [], 8),
|
||||
LocationRow('Burn some Oak Logs', 'firemaking', ['Oak Tree', ], [SkillRequirement('Firemaking', 15), ], [], 0),
|
||||
LocationRow('Burn some Willow Logs', 'firemaking', ['Willow Tree', ], [SkillRequirement('Firemaking', 30), ], [], 0),
|
||||
LocationRow('Burn some Oak Logs', 'firemaking', ['Oak Tree', ], [SkillRequirement('Firemaking', 15), SkillRequirement('Woodcutting', 15), ], [], 0),
|
||||
LocationRow('Burn some Willow Logs', 'firemaking', ['Willow Tree', ], [SkillRequirement('Firemaking', 30), SkillRequirement('Woodcutting', 30), ], [], 0),
|
||||
LocationRow('Travel on a Canoe', 'woodcutting', ['Canoe Tree', ], [SkillRequirement('Woodcutting', 12), ], [], 0),
|
||||
LocationRow('Cut an Oak Log', 'woodcutting', ['Oak Tree', ], [SkillRequirement('Woodcutting', 15), ], [], 0),
|
||||
LocationRow('Cut a Willow Log', 'woodcutting', ['Willow Tree', ], [SkillRequirement('Woodcutting', 30), ], [], 0),
|
||||
|
||||
@@ -31,7 +31,7 @@ class RegionNames(str, Enum):
|
||||
Mudskipper_Point = "Mudskipper Point"
|
||||
Karamja = "Karamja"
|
||||
Corsair_Cove = "Corsair Cove"
|
||||
Wilderness = "The Wilderness"
|
||||
Wilderness = "Wilderness"
|
||||
Crandor = "Crandor"
|
||||
# Resource Regions
|
||||
Egg = "Egg"
|
||||
|
||||
337
worlds/osrs/Rules.py
Normal file
337
worlds/osrs/Rules.py
Normal file
@@ -0,0 +1,337 @@
|
||||
"""
|
||||
Ensures a target level can be reached with available resources
|
||||
"""
|
||||
from worlds.generic.Rules import CollectionRule, add_rule
|
||||
from .Names import RegionNames, ItemNames
|
||||
|
||||
|
||||
def get_fishing_skill_rule(level, player, options) -> CollectionRule:
|
||||
if options.max_fishing_level < level:
|
||||
return lambda state: False
|
||||
|
||||
if options.brutal_grinds or level < 5:
|
||||
return lambda state: state.can_reach_region(RegionNames.Shrimp, player)
|
||||
if level < 20:
|
||||
return lambda state: state.can_reach_region(RegionNames.Shrimp, player) and \
|
||||
state.can_reach_region(RegionNames.Port_Sarim, player)
|
||||
else:
|
||||
return lambda state: state.can_reach_region(RegionNames.Shrimp, player) and \
|
||||
state.can_reach_region(RegionNames.Port_Sarim, player) and \
|
||||
state.can_reach_region(RegionNames.Fly_Fish, player)
|
||||
|
||||
|
||||
def get_mining_skill_rule(level, player, options) -> CollectionRule:
|
||||
if options.max_mining_level < level:
|
||||
return lambda state: False
|
||||
|
||||
if options.brutal_grinds or level < 15:
|
||||
return lambda state: state.can_reach_region(RegionNames.Bronze_Ores, player) or \
|
||||
state.can_reach_region(RegionNames.Clay_Rock, player)
|
||||
else:
|
||||
# Iron is the best way to train all the way to 99, so having access to iron is all you need to check for
|
||||
return lambda state: (state.can_reach_region(RegionNames.Bronze_Ores, player) or
|
||||
state.can_reach_region(RegionNames.Clay_Rock, player)) and \
|
||||
state.can_reach_region(RegionNames.Iron_Rock, player)
|
||||
|
||||
|
||||
def get_woodcutting_skill_rule(level, player, options) -> CollectionRule:
|
||||
if options.max_woodcutting_level < level:
|
||||
return lambda state: False
|
||||
|
||||
if options.brutal_grinds or level < 15:
|
||||
# I've checked. There is not a single chunk in the f2p that does not have at least one normal tree.
|
||||
# Even the desert.
|
||||
return lambda state: True
|
||||
if level < 30:
|
||||
return lambda state: state.can_reach_region(RegionNames.Oak_Tree, player)
|
||||
else:
|
||||
return lambda state: state.can_reach_region(RegionNames.Oak_Tree, player) and \
|
||||
state.can_reach_region(RegionNames.Willow_Tree, player)
|
||||
|
||||
|
||||
def get_smithing_skill_rule(level, player, options) -> CollectionRule:
|
||||
if options.max_smithing_level < level:
|
||||
return lambda state: False
|
||||
|
||||
if options.brutal_grinds:
|
||||
return lambda state: state.can_reach_region(RegionNames.Bronze_Ores, player) and \
|
||||
state.can_reach_region(RegionNames.Furnace, player)
|
||||
if level < 15:
|
||||
# Lumbridge has a special bronze-only anvil. This is the only anvil of its type so it's not included
|
||||
# in the "Anvil" resource region. We still need to check for it though.
|
||||
return lambda state: state.can_reach_region(RegionNames.Bronze_Ores, player) and \
|
||||
state.can_reach_region(RegionNames.Furnace, player) and \
|
||||
(state.can_reach_region(RegionNames.Anvil, player) or
|
||||
state.can_reach_region(RegionNames.Lumbridge, player))
|
||||
if level < 30:
|
||||
# For levels between 15 and 30, the lumbridge anvil won't cut it. Only a real one will do
|
||||
return lambda state: state.can_reach_region(RegionNames.Bronze_Ores, player) and \
|
||||
state.can_reach_region(RegionNames.Iron_Rock, player) and \
|
||||
state.can_reach_region(RegionNames.Furnace, player) and \
|
||||
state.can_reach_region(RegionNames.Anvil, player)
|
||||
else:
|
||||
return lambda state: state.can_reach_region(RegionNames.Bronze_Ores, player) and \
|
||||
state.can_reach_region(RegionNames.Iron_Rock, player) and \
|
||||
state.can_reach_region(RegionNames.Coal_Rock, player) and \
|
||||
state.can_reach_region(RegionNames.Furnace, player) and \
|
||||
state.can_reach_region(RegionNames.Anvil, player)
|
||||
|
||||
|
||||
def get_crafting_skill_rule(level, player, options):
|
||||
if options.max_crafting_level < level:
|
||||
return lambda state: False
|
||||
|
||||
# Crafting is really complex. Need a lot of sub-rules to make this even remotely readable
|
||||
def can_spin(state):
|
||||
return state.can_reach_region(RegionNames.Sheep, player) and \
|
||||
state.can_reach_region(RegionNames.Spinning_Wheel, player)
|
||||
|
||||
def can_pot(state):
|
||||
return state.can_reach_region(RegionNames.Clay_Rock, player) and \
|
||||
state.can_reach_region(RegionNames.Barbarian_Village, player)
|
||||
|
||||
def can_tan(state):
|
||||
return state.can_reach_region(RegionNames.Milk, player) and \
|
||||
state.can_reach_region(RegionNames.Al_Kharid, player)
|
||||
|
||||
def mould_access(state):
|
||||
return state.can_reach_region(RegionNames.Al_Kharid, player) or \
|
||||
state.can_reach_region(RegionNames.Rimmington, player)
|
||||
|
||||
def can_silver(state):
|
||||
return state.can_reach_region(RegionNames.Silver_Rock, player) and \
|
||||
state.can_reach_region(RegionNames.Furnace, player) and mould_access(state)
|
||||
|
||||
def can_gold(state):
|
||||
return state.can_reach_region(RegionNames.Gold_Rock, player) and \
|
||||
state.can_reach_region(RegionNames.Furnace, player) and mould_access(state)
|
||||
|
||||
if options.brutal_grinds or level < 5:
|
||||
return lambda state: can_spin(state) or can_pot(state) or can_tan(state)
|
||||
|
||||
can_smelt_gold = get_smithing_skill_rule(40, player, options)
|
||||
can_smelt_silver = get_smithing_skill_rule(20, player, options)
|
||||
if level < 16:
|
||||
return lambda state: can_pot(state) or can_tan(state) or (can_gold(state) and can_smelt_gold(state))
|
||||
else:
|
||||
return lambda state: can_tan(state) or (can_silver(state) and can_smelt_silver(state)) or \
|
||||
(can_gold(state) and can_smelt_gold(state))
|
||||
|
||||
|
||||
def get_cooking_skill_rule(level, player, options) -> CollectionRule:
|
||||
if options.max_cooking_level < level:
|
||||
return lambda state: False
|
||||
|
||||
if options.brutal_grinds or level < 15:
|
||||
return lambda state: state.can_reach_region(RegionNames.Milk, player) or \
|
||||
state.can_reach_region(RegionNames.Egg, player) or \
|
||||
state.can_reach_region(RegionNames.Shrimp, player) or \
|
||||
(state.can_reach_region(RegionNames.Wheat, player) and
|
||||
state.can_reach_region(RegionNames.Windmill, player))
|
||||
else:
|
||||
can_catch_fly_fish = get_fishing_skill_rule(20, player, options)
|
||||
|
||||
return lambda state: (
|
||||
(state.can_reach_region(RegionNames.Fly_Fish, player) and can_catch_fly_fish(state)) or
|
||||
(state.can_reach_region(RegionNames.Port_Sarim, player))
|
||||
) and (
|
||||
state.can_reach_region(RegionNames.Milk, player) or
|
||||
state.can_reach_region(RegionNames.Egg, player) or
|
||||
state.can_reach_region(RegionNames.Shrimp, player) or
|
||||
(state.can_reach_region(RegionNames.Wheat, player) and
|
||||
state.can_reach_region(RegionNames.Windmill, player))
|
||||
)
|
||||
|
||||
|
||||
def get_runecraft_skill_rule(level, player, options) -> CollectionRule:
|
||||
if options.max_runecraft_level < level:
|
||||
return lambda state: False
|
||||
if not options.brutal_grinds:
|
||||
# Ensure access to the relevant altars
|
||||
if level >= 5:
|
||||
return lambda state: state.has(ItemNames.QP_Rune_Mysteries, player) and \
|
||||
state.can_reach_region(RegionNames.Falador_Farm, player) and \
|
||||
state.can_reach_region(RegionNames.Lumbridge_Swamp, player)
|
||||
if level >= 9:
|
||||
return lambda state: state.has(ItemNames.QP_Rune_Mysteries, player) and \
|
||||
state.can_reach_region(RegionNames.Falador_Farm, player) and \
|
||||
state.can_reach_region(RegionNames.Lumbridge_Swamp, player) and \
|
||||
state.can_reach_region(RegionNames.East_Of_Varrock, player)
|
||||
if level >= 14:
|
||||
return lambda state: state.has(ItemNames.QP_Rune_Mysteries, player) and \
|
||||
state.can_reach_region(RegionNames.Falador_Farm, player) and \
|
||||
state.can_reach_region(RegionNames.Lumbridge_Swamp, player) and \
|
||||
state.can_reach_region(RegionNames.East_Of_Varrock, player) and \
|
||||
state.can_reach_region(RegionNames.Al_Kharid, player)
|
||||
|
||||
return lambda state: state.has(ItemNames.QP_Rune_Mysteries, player) and \
|
||||
state.can_reach_region(RegionNames.Falador_Farm, player)
|
||||
|
||||
|
||||
def get_magic_skill_rule(level, player, options) -> CollectionRule:
|
||||
if options.max_magic_level < level:
|
||||
return lambda state: False
|
||||
|
||||
return lambda state: state.can_reach_region(RegionNames.Mind_Runes, player)
|
||||
|
||||
|
||||
def get_firemaking_skill_rule(level, player, options) -> CollectionRule:
|
||||
if options.max_firemaking_level < level:
|
||||
return lambda state: False
|
||||
if not options.brutal_grinds:
|
||||
if level >= 30:
|
||||
can_chop_willows = get_woodcutting_skill_rule(30, player, options)
|
||||
return lambda state: state.can_reach_region(RegionNames.Willow_Tree, player) and can_chop_willows(state)
|
||||
if level >= 15:
|
||||
can_chop_oaks = get_woodcutting_skill_rule(15, player, options)
|
||||
return lambda state: state.can_reach_region(RegionNames.Oak_Tree, player) and can_chop_oaks(state)
|
||||
# If brutal grinds are on, or if the level is less than 15, you can train it.
|
||||
return lambda state: True
|
||||
|
||||
|
||||
def get_skill_rule(skill, level, player, options) -> CollectionRule:
|
||||
if skill.lower() == "fishing":
|
||||
return get_fishing_skill_rule(level, player, options)
|
||||
if skill.lower() == "mining":
|
||||
return get_mining_skill_rule(level, player, options)
|
||||
if skill.lower() == "woodcutting":
|
||||
return get_woodcutting_skill_rule(level, player, options)
|
||||
if skill.lower() == "smithing":
|
||||
return get_smithing_skill_rule(level, player, options)
|
||||
if skill.lower() == "crafting":
|
||||
return get_crafting_skill_rule(level, player, options)
|
||||
if skill.lower() == "cooking":
|
||||
return get_cooking_skill_rule(level, player, options)
|
||||
if skill.lower() == "runecraft":
|
||||
return get_runecraft_skill_rule(level, player, options)
|
||||
if skill.lower() == "magic":
|
||||
return get_magic_skill_rule(level, player, options)
|
||||
if skill.lower() == "firemaking":
|
||||
return get_firemaking_skill_rule(level, player, options)
|
||||
|
||||
return lambda state: True
|
||||
|
||||
|
||||
def generate_special_rules_for(entrance, region_row, outbound_region_name, player, options):
|
||||
if outbound_region_name == RegionNames.Cooks_Guild:
|
||||
add_rule(entrance, get_cooking_skill_rule(32, player, options))
|
||||
elif outbound_region_name == RegionNames.Crafting_Guild:
|
||||
add_rule(entrance, get_crafting_skill_rule(40, player, options))
|
||||
elif outbound_region_name == RegionNames.Corsair_Cove:
|
||||
# Need to be able to start Corsair Curse in addition to having the item
|
||||
add_rule(entrance, lambda state: state.can_reach(RegionNames.Falador_Farm, "Region", player))
|
||||
elif outbound_region_name == "Camdozaal*":
|
||||
add_rule(entrance, lambda state: state.has(ItemNames.QP_Below_Ice_Mountain, player))
|
||||
elif region_row.name == "Dwarven Mountain Pass" and outbound_region_name == "Anvil*":
|
||||
add_rule(entrance, lambda state: state.has(ItemNames.QP_Dorics_Quest, player))
|
||||
|
||||
# Special logic for canoes
|
||||
canoe_regions = [RegionNames.Lumbridge, RegionNames.South_Of_Varrock, RegionNames.Barbarian_Village,
|
||||
RegionNames.Edgeville, RegionNames.Wilderness]
|
||||
if region_row.name in canoe_regions:
|
||||
# Skill rules for greater distances
|
||||
woodcutting_rule_d1 = get_woodcutting_skill_rule(12, player, options)
|
||||
woodcutting_rule_d2 = get_woodcutting_skill_rule(27, player, options)
|
||||
woodcutting_rule_d3 = get_woodcutting_skill_rule(42, player, options)
|
||||
woodcutting_rule_all = get_woodcutting_skill_rule(57, player, options)
|
||||
|
||||
if region_row.name == RegionNames.Lumbridge:
|
||||
# Canoe Tree access for the Location
|
||||
if outbound_region_name == RegionNames.Canoe_Tree:
|
||||
add_rule(entrance,
|
||||
lambda state: (state.can_reach_region(RegionNames.South_Of_Varrock, player)
|
||||
and woodcutting_rule_d1(state)) or
|
||||
(state.can_reach_region(RegionNames.Barbarian_Village, player)
|
||||
and woodcutting_rule_d2(state)) or
|
||||
(state.can_reach_region(RegionNames.Edgeville, player)
|
||||
and woodcutting_rule_d3(state)) or
|
||||
(state.can_reach_region(RegionNames.Wilderness, player)
|
||||
and woodcutting_rule_all(state)))
|
||||
|
||||
# Access to other chunks based on woodcutting settings
|
||||
elif outbound_region_name == RegionNames.South_Of_Varrock:
|
||||
add_rule(entrance, woodcutting_rule_d1)
|
||||
elif outbound_region_name == RegionNames.Barbarian_Village:
|
||||
add_rule(entrance, woodcutting_rule_d2)
|
||||
elif outbound_region_name == RegionNames.Edgeville:
|
||||
add_rule(entrance, woodcutting_rule_d3)
|
||||
elif outbound_region_name == RegionNames.Wilderness:
|
||||
add_rule(entrance, woodcutting_rule_all)
|
||||
|
||||
elif region_row.name == RegionNames.South_Of_Varrock:
|
||||
if outbound_region_name == RegionNames.Canoe_Tree:
|
||||
add_rule(entrance,
|
||||
lambda state: (state.can_reach_region(RegionNames.Lumbridge, player)
|
||||
and woodcutting_rule_d1(state)) or
|
||||
(state.can_reach_region(RegionNames.Barbarian_Village, player)
|
||||
and woodcutting_rule_d1(state)) or
|
||||
(state.can_reach_region(RegionNames.Edgeville, player)
|
||||
and woodcutting_rule_d2(state)) or
|
||||
(state.can_reach_region(RegionNames.Wilderness, player)
|
||||
and woodcutting_rule_d3(state)))
|
||||
|
||||
# Access to other chunks based on woodcutting settings
|
||||
elif outbound_region_name == RegionNames.Lumbridge:
|
||||
add_rule(entrance, woodcutting_rule_d1)
|
||||
elif outbound_region_name == RegionNames.Barbarian_Village:
|
||||
add_rule(entrance, woodcutting_rule_d1)
|
||||
elif outbound_region_name == RegionNames.Edgeville:
|
||||
add_rule(entrance, woodcutting_rule_d3)
|
||||
elif outbound_region_name == RegionNames.Wilderness:
|
||||
add_rule(entrance, woodcutting_rule_all)
|
||||
elif region_row.name == RegionNames.Barbarian_Village:
|
||||
if outbound_region_name == RegionNames.Canoe_Tree:
|
||||
add_rule(entrance,
|
||||
lambda state: (state.can_reach_region(RegionNames.Lumbridge, player)
|
||||
and woodcutting_rule_d2(state)) or (state.can_reach_region(RegionNames.South_Of_Varrock, player)
|
||||
and woodcutting_rule_d1(state)) or (state.can_reach_region(RegionNames.Edgeville, player)
|
||||
and woodcutting_rule_d1(state)) or (state.can_reach_region(RegionNames.Wilderness, player)
|
||||
and woodcutting_rule_d2(state)))
|
||||
|
||||
# Access to other chunks based on woodcutting settings
|
||||
elif outbound_region_name == RegionNames.Lumbridge:
|
||||
add_rule(entrance, woodcutting_rule_d2)
|
||||
elif outbound_region_name == RegionNames.South_Of_Varrock:
|
||||
add_rule(entrance, woodcutting_rule_d1)
|
||||
# Edgeville does not need to be checked, because it's already adjacent
|
||||
elif outbound_region_name == RegionNames.Wilderness:
|
||||
add_rule(entrance, woodcutting_rule_d3)
|
||||
elif region_row.name == RegionNames.Edgeville:
|
||||
if outbound_region_name == RegionNames.Canoe_Tree:
|
||||
add_rule(entrance,
|
||||
lambda state: (state.can_reach_region(RegionNames.Lumbridge, player)
|
||||
and woodcutting_rule_d3(state)) or
|
||||
(state.can_reach_region(RegionNames.South_Of_Varrock, player)
|
||||
and woodcutting_rule_d2(state)) or
|
||||
(state.can_reach_region(RegionNames.Barbarian_Village, player)
|
||||
and woodcutting_rule_d1(state)) or
|
||||
(state.can_reach_region(RegionNames.Wilderness, player)
|
||||
and woodcutting_rule_d1(state)))
|
||||
|
||||
# Access to other chunks based on woodcutting settings
|
||||
elif outbound_region_name == RegionNames.Lumbridge:
|
||||
add_rule(entrance, woodcutting_rule_d3)
|
||||
elif outbound_region_name == RegionNames.South_Of_Varrock:
|
||||
add_rule(entrance, woodcutting_rule_d2)
|
||||
# Barbarian Village does not need to be checked, because it's already adjacent
|
||||
# Wilderness does not need to be checked, because it's already adjacent
|
||||
elif region_row.name == RegionNames.Wilderness:
|
||||
if outbound_region_name == RegionNames.Canoe_Tree:
|
||||
add_rule(entrance,
|
||||
lambda state: (state.can_reach_region(RegionNames.Lumbridge, player)
|
||||
and woodcutting_rule_all(state)) or
|
||||
(state.can_reach_region(RegionNames.South_Of_Varrock, player)
|
||||
and woodcutting_rule_d3(state)) or
|
||||
(state.can_reach_region(RegionNames.Barbarian_Village, player)
|
||||
and woodcutting_rule_d2(state)) or
|
||||
(state.can_reach_region(RegionNames.Edgeville, player)
|
||||
and woodcutting_rule_d1(state)))
|
||||
|
||||
# Access to other chunks based on woodcutting settings
|
||||
elif outbound_region_name == RegionNames.Lumbridge:
|
||||
add_rule(entrance, woodcutting_rule_all)
|
||||
elif outbound_region_name == RegionNames.South_Of_Varrock:
|
||||
add_rule(entrance, woodcutting_rule_d3)
|
||||
elif outbound_region_name == RegionNames.Barbarian_Village:
|
||||
add_rule(entrance, woodcutting_rule_d2)
|
||||
# Edgeville does not need to be checked, because it's already adjacent
|
||||
@@ -1,12 +1,12 @@
|
||||
import typing
|
||||
|
||||
from BaseClasses import Item, Tutorial, ItemClassification, Region, MultiWorld
|
||||
from BaseClasses import Item, Tutorial, ItemClassification, Region, MultiWorld, CollectionState
|
||||
from Fill import fill_restrictive, FillError
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
from worlds.generic.Rules import add_rule, CollectionRule
|
||||
from .Items import OSRSItem, starting_area_dict, chunksanity_starting_chunks, QP_Items, ItemRow, \
|
||||
chunksanity_special_region_names
|
||||
from .Locations import OSRSLocation, LocationRow
|
||||
|
||||
from .Rules import *
|
||||
from .Options import OSRSOptions, StartingArea
|
||||
from .Names import LocationNames, ItemNames, RegionNames
|
||||
|
||||
@@ -46,6 +46,7 @@ class OSRSWorld(World):
|
||||
web = OSRSWeb()
|
||||
base_id = 0x070000
|
||||
data_version = 1
|
||||
explicit_indirect_conditions = False
|
||||
|
||||
item_name_to_id = {item_rows[i].name: 0x070000 + i for i in range(len(item_rows))}
|
||||
location_name_to_id = {location_rows[i].name: 0x070000 + i for i in range(len(location_rows))}
|
||||
@@ -61,6 +62,7 @@ class OSRSWorld(World):
|
||||
starting_area_item: str
|
||||
|
||||
locations_by_category: typing.Dict[str, typing.List[LocationRow]]
|
||||
available_QP_locations: typing.List[str]
|
||||
|
||||
def __init__(self, multiworld: MultiWorld, player: int):
|
||||
super().__init__(multiworld, player)
|
||||
@@ -75,6 +77,7 @@ class OSRSWorld(World):
|
||||
self.starting_area_item = ""
|
||||
|
||||
self.locations_by_category = {}
|
||||
self.available_QP_locations = []
|
||||
|
||||
def generate_early(self) -> None:
|
||||
location_categories = [location_row.category for location_row in location_rows]
|
||||
@@ -90,9 +93,9 @@ class OSRSWorld(World):
|
||||
|
||||
rnd = self.random
|
||||
starting_area = self.options.starting_area
|
||||
|
||||
|
||||
#UT specific override, if we are in normal gen, resolve starting area, we will get it from slot_data in UT
|
||||
if not hasattr(self.multiworld, "generation_is_fake"):
|
||||
if not hasattr(self.multiworld, "generation_is_fake"):
|
||||
if starting_area.value == StartingArea.option_any_bank:
|
||||
self.starting_area_item = rnd.choice(starting_area_dict)
|
||||
elif starting_area.value < StartingArea.option_chunksanity:
|
||||
@@ -127,7 +130,6 @@ class OSRSWorld(World):
|
||||
starting_entrance.access_rule = lambda state: state.has(self.starting_area_item, self.player)
|
||||
starting_entrance.connect(self.region_name_to_data[starting_area_region])
|
||||
|
||||
|
||||
def create_regions(self) -> None:
|
||||
"""
|
||||
called to place player's regions into the MultiWorld's regions list. If it's hard to separate, this can be done
|
||||
@@ -145,7 +147,8 @@ class OSRSWorld(World):
|
||||
|
||||
# Removes the word "Area: " from the item name to get the region it applies to.
|
||||
# I figured tacking "Area: " at the beginning would make it _easier_ to tell apart. Turns out it made it worse
|
||||
if self.starting_area_item != "": #if area hasn't been set, then we shouldn't connect it
|
||||
# if area hasn't been set, then we shouldn't connect it
|
||||
if self.starting_area_item != "":
|
||||
if self.starting_area_item in chunksanity_special_region_names:
|
||||
starting_area_region = chunksanity_special_region_names[self.starting_area_item]
|
||||
else:
|
||||
@@ -164,11 +167,8 @@ class OSRSWorld(World):
|
||||
entrance.connect(self.region_name_to_data[parsed_outbound])
|
||||
|
||||
item_name = self.region_rows_by_name[parsed_outbound].itemReq
|
||||
if "*" not in outbound_region_name and "*" not in item_name:
|
||||
entrance.access_rule = lambda state, item_name=item_name: state.has(item_name, self.player)
|
||||
continue
|
||||
|
||||
self.generate_special_rules_for(entrance, region_row, outbound_region_name)
|
||||
entrance.access_rule = lambda state, item_name=item_name.replace("*",""): state.has(item_name, self.player)
|
||||
generate_special_rules_for(entrance, region_row, outbound_region_name, self.player, self.options)
|
||||
|
||||
for resource_region in region_row.resources:
|
||||
if not resource_region:
|
||||
@@ -178,321 +178,34 @@ class OSRSWorld(World):
|
||||
if "*" not in resource_region:
|
||||
entrance.connect(self.region_name_to_data[resource_region])
|
||||
else:
|
||||
self.generate_special_rules_for(entrance, region_row, resource_region)
|
||||
entrance.connect(self.region_name_to_data[resource_region.replace('*', '')])
|
||||
generate_special_rules_for(entrance, region_row, resource_region, self.player, self.options)
|
||||
|
||||
self.roll_locations()
|
||||
|
||||
def generate_special_rules_for(self, entrance, region_row, outbound_region_name):
|
||||
# print(f"Special rules required to access region {outbound_region_name} from {region_row.name}")
|
||||
if outbound_region_name == RegionNames.Cooks_Guild:
|
||||
item_name = self.region_rows_by_name[outbound_region_name].itemReq.replace('*', '')
|
||||
cooking_level_rule = self.get_skill_rule("cooking", 32)
|
||||
entrance.access_rule = lambda state: state.has(item_name, self.player) and \
|
||||
cooking_level_rule(state)
|
||||
if self.options.brutal_grinds:
|
||||
cooking_level_32_regions = {
|
||||
RegionNames.Milk,
|
||||
RegionNames.Egg,
|
||||
RegionNames.Shrimp,
|
||||
RegionNames.Wheat,
|
||||
RegionNames.Windmill,
|
||||
}
|
||||
else:
|
||||
# Level 15 cooking and higher requires level 20 fishing.
|
||||
fishing_level_20_regions = {
|
||||
RegionNames.Shrimp,
|
||||
RegionNames.Port_Sarim,
|
||||
}
|
||||
cooking_level_32_regions = {
|
||||
RegionNames.Milk,
|
||||
RegionNames.Egg,
|
||||
RegionNames.Shrimp,
|
||||
RegionNames.Wheat,
|
||||
RegionNames.Windmill,
|
||||
RegionNames.Fly_Fish,
|
||||
*fishing_level_20_regions,
|
||||
}
|
||||
for region_name in cooking_level_32_regions:
|
||||
self.multiworld.register_indirect_condition(self.get_region(region_name), entrance)
|
||||
return
|
||||
if outbound_region_name == RegionNames.Crafting_Guild:
|
||||
item_name = self.region_rows_by_name[outbound_region_name].itemReq.replace('*', '')
|
||||
crafting_level_rule = self.get_skill_rule("crafting", 40)
|
||||
entrance.access_rule = lambda state: state.has(item_name, self.player) and \
|
||||
crafting_level_rule(state)
|
||||
if self.options.brutal_grinds:
|
||||
crafting_level_40_regions = {
|
||||
# can_spin
|
||||
RegionNames.Sheep,
|
||||
RegionNames.Spinning_Wheel,
|
||||
# can_pot
|
||||
RegionNames.Clay_Rock,
|
||||
RegionNames.Barbarian_Village,
|
||||
# can_tan
|
||||
RegionNames.Milk,
|
||||
RegionNames.Al_Kharid,
|
||||
}
|
||||
else:
|
||||
mould_access_regions = {
|
||||
RegionNames.Al_Kharid,
|
||||
RegionNames.Rimmington,
|
||||
}
|
||||
smithing_level_20_regions = {
|
||||
RegionNames.Bronze_Ores,
|
||||
RegionNames.Iron_Rock,
|
||||
RegionNames.Furnace,
|
||||
RegionNames.Anvil,
|
||||
}
|
||||
smithing_level_40_regions = {
|
||||
*smithing_level_20_regions,
|
||||
RegionNames.Coal_Rock,
|
||||
}
|
||||
crafting_level_40_regions = {
|
||||
# can_tan
|
||||
RegionNames.Milk,
|
||||
RegionNames.Al_Kharid,
|
||||
# can_silver
|
||||
RegionNames.Silver_Rock,
|
||||
RegionNames.Furnace,
|
||||
*mould_access_regions,
|
||||
# can_smelt_silver
|
||||
*smithing_level_20_regions,
|
||||
# can_gold
|
||||
RegionNames.Gold_Rock,
|
||||
RegionNames.Furnace,
|
||||
*mould_access_regions,
|
||||
# can_smelt_gold
|
||||
*smithing_level_40_regions,
|
||||
}
|
||||
for region_name in crafting_level_40_regions:
|
||||
self.multiworld.register_indirect_condition(self.get_region(region_name), entrance)
|
||||
return
|
||||
if outbound_region_name == RegionNames.Corsair_Cove:
|
||||
item_name = self.region_rows_by_name[outbound_region_name].itemReq.replace('*', '')
|
||||
# Need to be able to start Corsair Curse in addition to having the item
|
||||
entrance.access_rule = lambda state: state.has(item_name, self.player) and \
|
||||
state.can_reach(RegionNames.Falador_Farm, "Region", self.player)
|
||||
self.multiworld.register_indirect_condition(
|
||||
self.multiworld.get_region(RegionNames.Falador_Farm, self.player), entrance)
|
||||
|
||||
return
|
||||
if outbound_region_name == "Camdozaal*":
|
||||
item_name = self.region_rows_by_name[outbound_region_name.replace('*', '')].itemReq
|
||||
entrance.access_rule = lambda state: state.has(item_name, self.player) and \
|
||||
state.has(ItemNames.QP_Below_Ice_Mountain, self.player)
|
||||
return
|
||||
if region_row.name == "Dwarven Mountain Pass" and outbound_region_name == "Anvil*":
|
||||
entrance.access_rule = lambda state: state.has(ItemNames.QP_Dorics_Quest, self.player)
|
||||
return
|
||||
# Special logic for canoes
|
||||
canoe_regions = [RegionNames.Lumbridge, RegionNames.South_Of_Varrock, RegionNames.Barbarian_Village,
|
||||
RegionNames.Edgeville, RegionNames.Wilderness]
|
||||
if region_row.name in canoe_regions:
|
||||
# Skill rules for greater distances
|
||||
woodcutting_rule_d1 = self.get_skill_rule("woodcutting", 12)
|
||||
woodcutting_rule_d2 = self.get_skill_rule("woodcutting", 27)
|
||||
woodcutting_rule_d3 = self.get_skill_rule("woodcutting", 42)
|
||||
woodcutting_rule_all = self.get_skill_rule("woodcutting", 57)
|
||||
|
||||
def add_indirect_conditions_for_woodcutting_levels(entrance, *levels: int):
|
||||
if self.options.brutal_grinds:
|
||||
# No access to specific regions required.
|
||||
return
|
||||
# Currently, each level requirement requires everything from the previous level requirements, so the
|
||||
# maximum level requirement can be taken.
|
||||
max_level = max(levels, default=0)
|
||||
max_level = min(max_level, self.options.max_woodcutting_level.value)
|
||||
if 15 <= max_level < 30:
|
||||
self.multiworld.register_indirect_condition(self.get_region(RegionNames.Oak_Tree), entrance)
|
||||
elif 30 <= max_level:
|
||||
self.multiworld.register_indirect_condition(self.get_region(RegionNames.Oak_Tree), entrance)
|
||||
self.multiworld.register_indirect_condition(self.get_region(RegionNames.Willow_Tree), entrance)
|
||||
|
||||
if region_row.name == RegionNames.Lumbridge:
|
||||
# Canoe Tree access for the Location
|
||||
if outbound_region_name == RegionNames.Canoe_Tree:
|
||||
entrance.access_rule = \
|
||||
lambda state: (state.can_reach_region(RegionNames.South_Of_Varrock, self.player)
|
||||
and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \
|
||||
(state.can_reach_region(RegionNames.Barbarian_Village)
|
||||
and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) or \
|
||||
(state.can_reach_region(RegionNames.Edgeville)
|
||||
and woodcutting_rule_d3(state) and self.options.max_woodcutting_level >= 42) or \
|
||||
(state.can_reach_region(RegionNames.Wilderness)
|
||||
and woodcutting_rule_all(state) and self.options.max_woodcutting_level >= 57)
|
||||
add_indirect_conditions_for_woodcutting_levels(entrance, 12, 27, 42, 57)
|
||||
self.multiworld.register_indirect_condition(
|
||||
self.multiworld.get_region(RegionNames.South_Of_Varrock, self.player), entrance)
|
||||
self.multiworld.register_indirect_condition(
|
||||
self.multiworld.get_region(RegionNames.Barbarian_Village, self.player), entrance)
|
||||
self.multiworld.register_indirect_condition(
|
||||
self.multiworld.get_region(RegionNames.Edgeville, self.player), entrance)
|
||||
self.multiworld.register_indirect_condition(
|
||||
self.multiworld.get_region(RegionNames.Wilderness, self.player), entrance)
|
||||
# Access to other chunks based on woodcutting settings
|
||||
# South of Varrock does not need to be checked, because it's already adjacent
|
||||
if outbound_region_name == RegionNames.Barbarian_Village:
|
||||
entrance.access_rule = lambda state: woodcutting_rule_d2(state) \
|
||||
and self.options.max_woodcutting_level >= 27
|
||||
add_indirect_conditions_for_woodcutting_levels(entrance, 27)
|
||||
if outbound_region_name == RegionNames.Edgeville:
|
||||
entrance.access_rule = lambda state: woodcutting_rule_d3(state) \
|
||||
and self.options.max_woodcutting_level >= 42
|
||||
add_indirect_conditions_for_woodcutting_levels(entrance, 42)
|
||||
if outbound_region_name == RegionNames.Wilderness:
|
||||
entrance.access_rule = lambda state: woodcutting_rule_all(state) \
|
||||
and self.options.max_woodcutting_level >= 57
|
||||
add_indirect_conditions_for_woodcutting_levels(entrance, 57)
|
||||
|
||||
if region_row.name == RegionNames.South_Of_Varrock:
|
||||
if outbound_region_name == RegionNames.Canoe_Tree:
|
||||
entrance.access_rule = \
|
||||
lambda state: (state.can_reach_region(RegionNames.Lumbridge, self.player)
|
||||
and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \
|
||||
(state.can_reach_region(RegionNames.Barbarian_Village)
|
||||
and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \
|
||||
(state.can_reach_region(RegionNames.Edgeville)
|
||||
and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) or \
|
||||
(state.can_reach_region(RegionNames.Wilderness)
|
||||
and woodcutting_rule_d3(state) and self.options.max_woodcutting_level >= 42)
|
||||
add_indirect_conditions_for_woodcutting_levels(entrance, 12, 27, 42)
|
||||
self.multiworld.register_indirect_condition(
|
||||
self.multiworld.get_region(RegionNames.Lumbridge, self.player), entrance)
|
||||
self.multiworld.register_indirect_condition(
|
||||
self.multiworld.get_region(RegionNames.Barbarian_Village, self.player), entrance)
|
||||
self.multiworld.register_indirect_condition(
|
||||
self.multiworld.get_region(RegionNames.Edgeville, self.player), entrance)
|
||||
self.multiworld.register_indirect_condition(
|
||||
self.multiworld.get_region(RegionNames.Wilderness, self.player), entrance)
|
||||
# Access to other chunks based on woodcutting settings
|
||||
# Lumbridge does not need to be checked, because it's already adjacent
|
||||
if outbound_region_name == RegionNames.Barbarian_Village:
|
||||
entrance.access_rule = lambda state: woodcutting_rule_d1(state) \
|
||||
and self.options.max_woodcutting_level >= 12
|
||||
add_indirect_conditions_for_woodcutting_levels(entrance, 12)
|
||||
if outbound_region_name == RegionNames.Edgeville:
|
||||
entrance.access_rule = lambda state: woodcutting_rule_d3(state) \
|
||||
and self.options.max_woodcutting_level >= 27
|
||||
add_indirect_conditions_for_woodcutting_levels(entrance, 27)
|
||||
if outbound_region_name == RegionNames.Wilderness:
|
||||
entrance.access_rule = lambda state: woodcutting_rule_all(state) \
|
||||
and self.options.max_woodcutting_level >= 42
|
||||
add_indirect_conditions_for_woodcutting_levels(entrance, 42)
|
||||
if region_row.name == RegionNames.Barbarian_Village:
|
||||
if outbound_region_name == RegionNames.Canoe_Tree:
|
||||
entrance.access_rule = \
|
||||
lambda state: (state.can_reach_region(RegionNames.Lumbridge, self.player)
|
||||
and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) or \
|
||||
(state.can_reach_region(RegionNames.South_Of_Varrock)
|
||||
and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \
|
||||
(state.can_reach_region(RegionNames.Edgeville)
|
||||
and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \
|
||||
(state.can_reach_region(RegionNames.Wilderness)
|
||||
and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27)
|
||||
add_indirect_conditions_for_woodcutting_levels(entrance, 12, 27)
|
||||
self.multiworld.register_indirect_condition(
|
||||
self.multiworld.get_region(RegionNames.Lumbridge, self.player), entrance)
|
||||
self.multiworld.register_indirect_condition(
|
||||
self.multiworld.get_region(RegionNames.South_Of_Varrock, self.player), entrance)
|
||||
self.multiworld.register_indirect_condition(
|
||||
self.multiworld.get_region(RegionNames.Edgeville, self.player), entrance)
|
||||
self.multiworld.register_indirect_condition(
|
||||
self.multiworld.get_region(RegionNames.Wilderness, self.player), entrance)
|
||||
# Access to other chunks based on woodcutting settings
|
||||
if outbound_region_name == RegionNames.Lumbridge:
|
||||
entrance.access_rule = lambda state: woodcutting_rule_d2(state) \
|
||||
and self.options.max_woodcutting_level >= 27
|
||||
add_indirect_conditions_for_woodcutting_levels(entrance, 27)
|
||||
if outbound_region_name == RegionNames.South_Of_Varrock:
|
||||
entrance.access_rule = lambda state: woodcutting_rule_d1(state) \
|
||||
and self.options.max_woodcutting_level >= 12
|
||||
add_indirect_conditions_for_woodcutting_levels(entrance, 12)
|
||||
# Edgeville does not need to be checked, because it's already adjacent
|
||||
if outbound_region_name == RegionNames.Wilderness:
|
||||
entrance.access_rule = lambda state: woodcutting_rule_d3(state) \
|
||||
and self.options.max_woodcutting_level >= 42
|
||||
add_indirect_conditions_for_woodcutting_levels(entrance, 42)
|
||||
if region_row.name == RegionNames.Edgeville:
|
||||
if outbound_region_name == RegionNames.Canoe_Tree:
|
||||
entrance.access_rule = \
|
||||
lambda state: (state.can_reach_region(RegionNames.Lumbridge, self.player)
|
||||
and woodcutting_rule_d3(state) and self.options.max_woodcutting_level >= 42) or \
|
||||
(state.can_reach_region(RegionNames.South_Of_Varrock)
|
||||
and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) or \
|
||||
(state.can_reach_region(RegionNames.Barbarian_Village)
|
||||
and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12) or \
|
||||
(state.can_reach_region(RegionNames.Wilderness)
|
||||
and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12)
|
||||
add_indirect_conditions_for_woodcutting_levels(entrance, 12, 27, 42)
|
||||
self.multiworld.register_indirect_condition(
|
||||
self.multiworld.get_region(RegionNames.Lumbridge, self.player), entrance)
|
||||
self.multiworld.register_indirect_condition(
|
||||
self.multiworld.get_region(RegionNames.South_Of_Varrock, self.player), entrance)
|
||||
self.multiworld.register_indirect_condition(
|
||||
self.multiworld.get_region(RegionNames.Barbarian_Village, self.player), entrance)
|
||||
self.multiworld.register_indirect_condition(
|
||||
self.multiworld.get_region(RegionNames.Wilderness, self.player), entrance)
|
||||
# Access to other chunks based on woodcutting settings
|
||||
if outbound_region_name == RegionNames.Lumbridge:
|
||||
entrance.access_rule = lambda state: woodcutting_rule_d3(state) \
|
||||
and self.options.max_woodcutting_level >= 42
|
||||
add_indirect_conditions_for_woodcutting_levels(entrance, 42)
|
||||
if outbound_region_name == RegionNames.South_Of_Varrock:
|
||||
entrance.access_rule = lambda state: woodcutting_rule_d2(state) \
|
||||
and self.options.max_woodcutting_level >= 27
|
||||
add_indirect_conditions_for_woodcutting_levels(entrance, 27)
|
||||
# Barbarian Village does not need to be checked, because it's already adjacent
|
||||
# Wilderness does not need to be checked, because it's already adjacent
|
||||
if region_row.name == RegionNames.Wilderness:
|
||||
if outbound_region_name == RegionNames.Canoe_Tree:
|
||||
entrance.access_rule = \
|
||||
lambda state: (state.can_reach_region(RegionNames.Lumbridge, self.player)
|
||||
and woodcutting_rule_all(state) and self.options.max_woodcutting_level >= 57) or \
|
||||
(state.can_reach_region(RegionNames.South_Of_Varrock)
|
||||
and woodcutting_rule_d3(state) and self.options.max_woodcutting_level >= 42) or \
|
||||
(state.can_reach_region(RegionNames.Barbarian_Village)
|
||||
and woodcutting_rule_d2(state) and self.options.max_woodcutting_level >= 27) or \
|
||||
(state.can_reach_region(RegionNames.Edgeville)
|
||||
and woodcutting_rule_d1(state) and self.options.max_woodcutting_level >= 12)
|
||||
add_indirect_conditions_for_woodcutting_levels(entrance, 12, 27, 42, 57)
|
||||
self.multiworld.register_indirect_condition(
|
||||
self.multiworld.get_region(RegionNames.Lumbridge, self.player), entrance)
|
||||
self.multiworld.register_indirect_condition(
|
||||
self.multiworld.get_region(RegionNames.South_Of_Varrock, self.player), entrance)
|
||||
self.multiworld.register_indirect_condition(
|
||||
self.multiworld.get_region(RegionNames.Barbarian_Village, self.player), entrance)
|
||||
self.multiworld.register_indirect_condition(
|
||||
self.multiworld.get_region(RegionNames.Edgeville, self.player), entrance)
|
||||
# Access to other chunks based on woodcutting settings
|
||||
if outbound_region_name == RegionNames.Lumbridge:
|
||||
entrance.access_rule = lambda state: woodcutting_rule_all(state) \
|
||||
and self.options.max_woodcutting_level >= 57
|
||||
add_indirect_conditions_for_woodcutting_levels(entrance, 57)
|
||||
if outbound_region_name == RegionNames.South_Of_Varrock:
|
||||
entrance.access_rule = lambda state: woodcutting_rule_d3(state) \
|
||||
and self.options.max_woodcutting_level >= 42
|
||||
add_indirect_conditions_for_woodcutting_levels(entrance, 42)
|
||||
if outbound_region_name == RegionNames.Barbarian_Village:
|
||||
entrance.access_rule = lambda state: woodcutting_rule_d2(state) \
|
||||
and self.options.max_woodcutting_level >= 27
|
||||
add_indirect_conditions_for_woodcutting_levels(entrance, 27)
|
||||
# Edgeville does not need to be checked, because it's already adjacent
|
||||
def task_within_skill_levels(self, skills_required):
|
||||
# Loop through each required skill. If any of its requirements are out of the defined limit, return false
|
||||
for skill in skills_required:
|
||||
max_level_for_skill = getattr(self.options, f"max_{skill.skill.lower()}_level")
|
||||
if skill.level > max_level_for_skill:
|
||||
return False
|
||||
return True
|
||||
|
||||
def roll_locations(self):
|
||||
locations_required = 0
|
||||
generation_is_fake = hasattr(self.multiworld, "generation_is_fake") # UT specific override
|
||||
locations_required = 0
|
||||
for item_row in item_rows:
|
||||
locations_required += item_row.amount
|
||||
|
||||
locations_added = 1 # At this point we've already added the starting area, so we start at 1 instead of 0
|
||||
|
||||
# Quests are always added
|
||||
# Quests are always added first, before anything else is rolled
|
||||
for i, location_row in enumerate(location_rows):
|
||||
if location_row.category in {"quest", "points", "goal"}:
|
||||
self.create_and_add_location(i)
|
||||
if location_row.category == "quest":
|
||||
locations_added += 1
|
||||
if self.task_within_skill_levels(location_row.skills):
|
||||
self.create_and_add_location(i)
|
||||
if location_row.category == "quest":
|
||||
locations_added += 1
|
||||
|
||||
# Build up the weighted Task Pool
|
||||
rnd = self.random
|
||||
@@ -516,10 +229,9 @@ class OSRSWorld(World):
|
||||
task_types = ["prayer", "magic", "runecraft", "mining", "crafting",
|
||||
"smithing", "fishing", "cooking", "firemaking", "woodcutting", "combat"]
|
||||
for task_type in task_types:
|
||||
max_level_for_task_type = getattr(self.options, f"max_{task_type}_level")
|
||||
max_amount_for_task_type = getattr(self.options, f"max_{task_type}_tasks")
|
||||
tasks_for_this_type = [task for task in self.locations_by_category[task_type]
|
||||
if task.skills[0].level <= max_level_for_task_type]
|
||||
if self.task_within_skill_levels(task.skills)]
|
||||
if not self.options.progressive_tasks:
|
||||
rnd.shuffle(tasks_for_this_type)
|
||||
else:
|
||||
@@ -568,6 +280,7 @@ class OSRSWorld(World):
|
||||
self.add_location(task)
|
||||
locations_added += 1
|
||||
|
||||
|
||||
def add_location(self, location):
|
||||
index = [i for i in range(len(location_rows)) if location_rows[i].name == location.name][0]
|
||||
self.create_and_add_location(index)
|
||||
@@ -586,11 +299,15 @@ class OSRSWorld(World):
|
||||
|
||||
def create_and_add_location(self, row_index) -> None:
|
||||
location_row = location_rows[row_index]
|
||||
# print(f"Adding task {location_row.name}")
|
||||
|
||||
# Quest Points are handled differently now, but in case this gets fed an older version of the data sheet,
|
||||
# the points might still be listed in a different row
|
||||
if location_row.category == "points":
|
||||
return
|
||||
|
||||
# Create Location
|
||||
location_id = self.base_id + row_index
|
||||
if location_row.category == "points" or location_row.category == "goal":
|
||||
if location_row.category == "goal":
|
||||
location_id = None
|
||||
location = OSRSLocation(self.player, location_row.name, location_id)
|
||||
self.location_name_to_data[location_row.name] = location
|
||||
@@ -602,6 +319,14 @@ class OSRSWorld(World):
|
||||
location.parent_region = region
|
||||
region.locations.append(location)
|
||||
|
||||
# If it's a quest, generate a "Points" location we'll add an event to
|
||||
if location_row.category == "quest":
|
||||
points_name = location_row.name.replace("Quest:", "Points:")
|
||||
points_location = OSRSLocation(self.player, points_name)
|
||||
self.location_name_to_data[points_name] = points_location
|
||||
points_location.parent_region = region
|
||||
region.locations.append(points_location)
|
||||
|
||||
def set_rules(self) -> None:
|
||||
"""
|
||||
called to set access and item rules on locations and entrances.
|
||||
@@ -612,18 +337,26 @@ class OSRSWorld(World):
|
||||
"Witchs_Potion", "Knights_Sword", "Goblin_Diplomacy", "Pirates_Treasure",
|
||||
"Rune_Mysteries", "Misthalin_Mystery", "Corsair_Curse", "X_Marks_the_Spot",
|
||||
"Below_Ice_Mountain"]
|
||||
for qp_attr_name in quest_attr_names:
|
||||
loc_name = getattr(LocationNames, f"QP_{qp_attr_name}")
|
||||
item_name = getattr(ItemNames, f"QP_{qp_attr_name}")
|
||||
self.multiworld.get_location(loc_name, self.player) \
|
||||
.place_locked_item(self.create_event(item_name))
|
||||
|
||||
for quest_attr_name in quest_attr_names:
|
||||
qp_loc_name = getattr(LocationNames, f"QP_{quest_attr_name}")
|
||||
qp_loc = self.location_name_to_data.get(qp_loc_name)
|
||||
|
||||
q_loc_name = getattr(LocationNames, f"Q_{quest_attr_name}")
|
||||
add_rule(self.multiworld.get_location(qp_loc_name, self.player), lambda state, q_loc_name=q_loc_name: (
|
||||
self.multiworld.get_location(q_loc_name, self.player).can_reach(state)
|
||||
))
|
||||
q_loc = self.location_name_to_data.get(q_loc_name)
|
||||
|
||||
# Checks to make sure the task is actually in the list before trying to create its rules
|
||||
if qp_loc and q_loc:
|
||||
# Create the QP Event Item
|
||||
item_name = getattr(ItemNames, f"QP_{quest_attr_name}")
|
||||
qp_loc.place_locked_item(self.create_event(item_name))
|
||||
|
||||
# If a quest is excluded, don't actually consider it for quest point progression
|
||||
if q_loc_name not in self.options.exclude_locations:
|
||||
self.available_QP_locations.append(item_name)
|
||||
|
||||
# Set the access rule for the QP Location
|
||||
add_rule(qp_loc, lambda state, loc=q_loc: (loc.can_reach(state)))
|
||||
|
||||
# place "Victory" at "Dragon Slayer" and set collection as win condition
|
||||
self.multiworld.get_location(LocationNames.Q_Dragon_Slayer, self.player) \
|
||||
@@ -639,7 +372,7 @@ class OSRSWorld(World):
|
||||
lambda state, region_required=region_required: state.can_reach(region_required, "Region",
|
||||
self.player))
|
||||
for skill_req in location_row.skills:
|
||||
add_rule(location, self.get_skill_rule(skill_req.skill, skill_req.level))
|
||||
add_rule(location, get_skill_rule(skill_req.skill, skill_req.level, self.player, self.options))
|
||||
for item_req in location_row.items:
|
||||
add_rule(location, lambda state, item_req=item_req: state.has(item_req, self.player))
|
||||
if location_row.qp:
|
||||
@@ -664,124 +397,8 @@ class OSRSWorld(World):
|
||||
|
||||
def quest_points(self, state):
|
||||
qp = 0
|
||||
for qp_event in QP_Items:
|
||||
for qp_event in self.available_QP_locations:
|
||||
if state.has(qp_event, self.player):
|
||||
qp += int(qp_event[0])
|
||||
return qp
|
||||
|
||||
"""
|
||||
Ensures a target level can be reached with available resources
|
||||
"""
|
||||
|
||||
def get_skill_rule(self, skill, level) -> CollectionRule:
|
||||
if skill.lower() == "fishing":
|
||||
if self.options.brutal_grinds or level < 5:
|
||||
return lambda state: state.can_reach(RegionNames.Shrimp, "Region", self.player)
|
||||
if level < 20:
|
||||
return lambda state: state.can_reach(RegionNames.Shrimp, "Region", self.player) and \
|
||||
state.can_reach(RegionNames.Port_Sarim, "Region", self.player)
|
||||
else:
|
||||
return lambda state: state.can_reach(RegionNames.Shrimp, "Region", self.player) and \
|
||||
state.can_reach(RegionNames.Port_Sarim, "Region", self.player) and \
|
||||
state.can_reach(RegionNames.Fly_Fish, "Region", self.player)
|
||||
if skill.lower() == "mining":
|
||||
if self.options.brutal_grinds or level < 15:
|
||||
return lambda state: state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) or \
|
||||
state.can_reach(RegionNames.Clay_Rock, "Region", self.player)
|
||||
else:
|
||||
# Iron is the best way to train all the way to 99, so having access to iron is all you need to check for
|
||||
return lambda state: (state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) or
|
||||
state.can_reach(RegionNames.Clay_Rock, "Region", self.player)) and \
|
||||
state.can_reach(RegionNames.Iron_Rock, "Region", self.player)
|
||||
if skill.lower() == "woodcutting":
|
||||
if self.options.brutal_grinds or level < 15:
|
||||
# I've checked. There is not a single chunk in the f2p that does not have at least one normal tree.
|
||||
# Even the desert.
|
||||
return lambda state: True
|
||||
if level < 30:
|
||||
return lambda state: state.can_reach(RegionNames.Oak_Tree, "Region", self.player)
|
||||
else:
|
||||
return lambda state: state.can_reach(RegionNames.Oak_Tree, "Region", self.player) and \
|
||||
state.can_reach(RegionNames.Willow_Tree, "Region", self.player)
|
||||
if skill.lower() == "smithing":
|
||||
if self.options.brutal_grinds:
|
||||
return lambda state: state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) and \
|
||||
state.can_reach(RegionNames.Furnace, "Region", self.player)
|
||||
if level < 15:
|
||||
# Lumbridge has a special bronze-only anvil. This is the only anvil of its type so it's not included
|
||||
# in the "Anvil" resource region. We still need to check for it though.
|
||||
return lambda state: state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) and \
|
||||
state.can_reach(RegionNames.Furnace, "Region", self.player) and \
|
||||
(state.can_reach(RegionNames.Anvil, "Region", self.player) or
|
||||
state.can_reach(RegionNames.Lumbridge, "Region", self.player))
|
||||
if level < 30:
|
||||
# For levels between 15 and 30, the lumbridge anvil won't cut it. Only a real one will do
|
||||
return lambda state: state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) and \
|
||||
state.can_reach(RegionNames.Iron_Rock, "Region", self.player) and \
|
||||
state.can_reach(RegionNames.Furnace, "Region", self.player) and \
|
||||
state.can_reach(RegionNames.Anvil, "Region", self.player)
|
||||
else:
|
||||
return lambda state: state.can_reach(RegionNames.Bronze_Ores, "Region", self.player) and \
|
||||
state.can_reach(RegionNames.Iron_Rock, "Region", self.player) and \
|
||||
state.can_reach(RegionNames.Coal_Rock, "Region", self.player) and \
|
||||
state.can_reach(RegionNames.Furnace, "Region", self.player) and \
|
||||
state.can_reach(RegionNames.Anvil, "Region", self.player)
|
||||
if skill.lower() == "crafting":
|
||||
# Crafting is really complex. Need a lot of sub-rules to make this even remotely readable
|
||||
def can_spin(state):
|
||||
return state.can_reach(RegionNames.Sheep, "Region", self.player) and \
|
||||
state.can_reach(RegionNames.Spinning_Wheel, "Region", self.player)
|
||||
|
||||
def can_pot(state):
|
||||
return state.can_reach(RegionNames.Clay_Rock, "Region", self.player) and \
|
||||
state.can_reach(RegionNames.Barbarian_Village, "Region", self.player)
|
||||
|
||||
def can_tan(state):
|
||||
return state.can_reach(RegionNames.Milk, "Region", self.player) and \
|
||||
state.can_reach(RegionNames.Al_Kharid, "Region", self.player)
|
||||
|
||||
def mould_access(state):
|
||||
return state.can_reach(RegionNames.Al_Kharid, "Region", self.player) or \
|
||||
state.can_reach(RegionNames.Rimmington, "Region", self.player)
|
||||
|
||||
def can_silver(state):
|
||||
|
||||
return state.can_reach(RegionNames.Silver_Rock, "Region", self.player) and \
|
||||
state.can_reach(RegionNames.Furnace, "Region", self.player) and mould_access(state)
|
||||
|
||||
def can_gold(state):
|
||||
return state.can_reach(RegionNames.Gold_Rock, "Region", self.player) and \
|
||||
state.can_reach(RegionNames.Furnace, "Region", self.player) and mould_access(state)
|
||||
|
||||
if self.options.brutal_grinds or level < 5:
|
||||
return lambda state: can_spin(state) or can_pot(state) or can_tan(state)
|
||||
|
||||
can_smelt_gold = self.get_skill_rule("smithing", 40)
|
||||
can_smelt_silver = self.get_skill_rule("smithing", 20)
|
||||
if level < 16:
|
||||
return lambda state: can_pot(state) or can_tan(state) or (can_gold(state) and can_smelt_gold(state))
|
||||
else:
|
||||
return lambda state: can_tan(state) or (can_silver(state) and can_smelt_silver(state)) or \
|
||||
(can_gold(state) and can_smelt_gold(state))
|
||||
if skill.lower() == "cooking":
|
||||
if self.options.brutal_grinds or level < 15:
|
||||
return lambda state: state.can_reach(RegionNames.Milk, "Region", self.player) or \
|
||||
state.can_reach(RegionNames.Egg, "Region", self.player) or \
|
||||
state.can_reach(RegionNames.Shrimp, "Region", self.player) or \
|
||||
(state.can_reach(RegionNames.Wheat, "Region", self.player) and
|
||||
state.can_reach(RegionNames.Windmill, "Region", self.player))
|
||||
else:
|
||||
can_catch_fly_fish = self.get_skill_rule("fishing", 20)
|
||||
return lambda state: state.can_reach(RegionNames.Fly_Fish, "Region", self.player) and \
|
||||
can_catch_fly_fish(state) and \
|
||||
(state.can_reach(RegionNames.Milk, "Region", self.player) or
|
||||
state.can_reach(RegionNames.Egg, "Region", self.player) or
|
||||
state.can_reach(RegionNames.Shrimp, "Region", self.player) or
|
||||
(state.can_reach(RegionNames.Wheat, "Region", self.player) and
|
||||
state.can_reach(RegionNames.Windmill, "Region", self.player)))
|
||||
if skill.lower() == "runecraft":
|
||||
return lambda state: state.has(ItemNames.QP_Rune_Mysteries, self.player)
|
||||
if skill.lower() == "magic":
|
||||
return lambda state: state.can_reach(RegionNames.Mind_Runes, "Region", self.player)
|
||||
|
||||
return lambda state: True
|
||||
|
||||
@@ -1387,7 +1387,7 @@ def get_locations(world: Optional[World]) -> Tuple[LocationData, ...]:
|
||||
lambda state: logic.templars_return_requirement(state)),
|
||||
LocationData("The Host", "The Host: Victory", SC2LOTV_LOC_ID_OFFSET + 2100, LocationType.VICTORY,
|
||||
lambda state: logic.the_host_requirement(state)),
|
||||
LocationData("The Host", "The Host: Southeast Void Shard", SC2LOTV_LOC_ID_OFFSET + 2101, LocationType.VICTORY,
|
||||
LocationData("The Host", "The Host: Southeast Void Shard", SC2LOTV_LOC_ID_OFFSET + 2101, LocationType.EXTRA,
|
||||
lambda state: logic.the_host_requirement(state)),
|
||||
LocationData("The Host", "The Host: South Void Shard", SC2LOTV_LOC_ID_OFFSET + 2102, LocationType.EXTRA,
|
||||
lambda state: logic.the_host_requirement(state)),
|
||||
|
||||
@@ -43,6 +43,9 @@ class SC2Campaign(Enum):
|
||||
self.goal_priority = goal_priority
|
||||
self.race = race
|
||||
|
||||
def __lt__(self, other: "SC2Campaign"):
|
||||
return self.id < other.id
|
||||
|
||||
GLOBAL = 0, "Global", SC2CampaignGoalPriority.NONE, SC2Race.ANY
|
||||
WOL = 1, "Wings of Liberty", SC2CampaignGoalPriority.VERY_HARD, SC2Race.TERRAN
|
||||
PROPHECY = 2, "Prophecy", SC2CampaignGoalPriority.MINI_CAMPAIGN, SC2Race.PROTOSS
|
||||
|
||||
@@ -50,7 +50,7 @@ def create_vanilla_regions(
|
||||
names: Dict[str, int] = {}
|
||||
|
||||
# Generating all regions and locations for each enabled campaign
|
||||
for campaign in enabled_campaigns:
|
||||
for campaign in sorted(enabled_campaigns):
|
||||
for region_name in vanilla_mission_req_table[campaign].keys():
|
||||
regions.append(create_region(world, locations_per_region, location_cache, region_name))
|
||||
world.multiworld.regions += regions
|
||||
|
||||
@@ -319,7 +319,7 @@ class StardewValleyWorld(World):
|
||||
if override_classification is None:
|
||||
override_classification = item.classification
|
||||
|
||||
if override_classification == ItemClassification.progression:
|
||||
if override_classification & ItemClassification.progression:
|
||||
self.total_progression_items += 1
|
||||
return StardewItem(item.name, override_classification, item.code, self.player)
|
||||
|
||||
|
||||
@@ -1,16 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from graphlib import TopologicalSorter
|
||||
from typing import Iterable, Mapping, Callable
|
||||
|
||||
from .game_content import StardewContent, ContentPack, StardewFeatures
|
||||
from .vanilla.base import base_game as base_game_content_pack
|
||||
from ..data.game_item import GameItem, ItemSource
|
||||
|
||||
try:
|
||||
from graphlib import TopologicalSorter
|
||||
except ImportError:
|
||||
from graphlib_backport import TopologicalSorter # noqa
|
||||
|
||||
|
||||
def unpack_content(features: StardewFeatures, packs: Iterable[ContentPack]) -> StardewContent:
|
||||
# Base game is always registered first.
|
||||
|
||||
@@ -1,9 +1,9 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from .game_item import kw_only, ItemSource
|
||||
from .game_item import ItemSource
|
||||
|
||||
|
||||
@dataclass(frozen=True, **kw_only)
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class MachineSource(ItemSource):
|
||||
item: str # this should be optional (worm bin)
|
||||
machine: str
|
||||
|
||||
@@ -1,5 +1,4 @@
|
||||
import enum
|
||||
import sys
|
||||
from abc import ABC
|
||||
from dataclasses import dataclass, field
|
||||
from types import MappingProxyType
|
||||
@@ -7,11 +6,6 @@ from typing import List, Iterable, Set, ClassVar, Tuple, Mapping, Callable, Any
|
||||
|
||||
from ..stardew_rule.protocol import StardewRule
|
||||
|
||||
if sys.version_info >= (3, 10):
|
||||
kw_only = {"kw_only": True}
|
||||
else:
|
||||
kw_only = {}
|
||||
|
||||
DEFAULT_REQUIREMENT_TAGS = MappingProxyType({})
|
||||
|
||||
|
||||
@@ -36,21 +30,17 @@ class ItemTag(enum.Enum):
|
||||
class ItemSource(ABC):
|
||||
add_tags: ClassVar[Tuple[ItemTag]] = ()
|
||||
|
||||
other_requirements: Tuple[Requirement, ...] = field(kw_only=True, default_factory=tuple)
|
||||
|
||||
@property
|
||||
def requirement_tags(self) -> Mapping[str, Tuple[ItemTag, ...]]:
|
||||
return DEFAULT_REQUIREMENT_TAGS
|
||||
|
||||
# FIXME this should just be an optional field, but kw_only requires python 3.10...
|
||||
@property
|
||||
def other_requirements(self) -> Iterable[Requirement]:
|
||||
return ()
|
||||
|
||||
|
||||
@dataclass(frozen=True, **kw_only)
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class GenericSource(ItemSource):
|
||||
regions: Tuple[str, ...] = ()
|
||||
"""No region means it's available everywhere."""
|
||||
other_requirements: Tuple[Requirement, ...] = ()
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
@@ -59,7 +49,7 @@ class CustomRuleSource(ItemSource):
|
||||
create_rule: Callable[[Any], StardewRule]
|
||||
|
||||
|
||||
@dataclass(frozen=True, **kw_only)
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class CompoundSource(ItemSource):
|
||||
sources: Tuple[ItemSource, ...] = ()
|
||||
|
||||
|
||||
@@ -1,18 +1,17 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Tuple, Sequence, Mapping
|
||||
|
||||
from .game_item import ItemSource, kw_only, ItemTag, Requirement
|
||||
from .game_item import ItemSource, ItemTag
|
||||
from ..strings.season_names import Season
|
||||
|
||||
|
||||
@dataclass(frozen=True, **kw_only)
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class ForagingSource(ItemSource):
|
||||
regions: Tuple[str, ...]
|
||||
seasons: Tuple[str, ...] = Season.all
|
||||
other_requirements: Tuple[Requirement, ...] = ()
|
||||
|
||||
|
||||
@dataclass(frozen=True, **kw_only)
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class SeasonalForagingSource(ItemSource):
|
||||
season: str
|
||||
days: Sequence[int]
|
||||
@@ -22,17 +21,17 @@ class SeasonalForagingSource(ItemSource):
|
||||
return ForagingSource(seasons=(self.season,), regions=self.regions)
|
||||
|
||||
|
||||
@dataclass(frozen=True, **kw_only)
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class FruitBatsSource(ItemSource):
|
||||
...
|
||||
|
||||
|
||||
@dataclass(frozen=True, **kw_only)
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class MushroomCaveSource(ItemSource):
|
||||
...
|
||||
|
||||
|
||||
@dataclass(frozen=True, **kw_only)
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class HarvestFruitTreeSource(ItemSource):
|
||||
add_tags = (ItemTag.CROPSANITY,)
|
||||
|
||||
@@ -46,7 +45,7 @@ class HarvestFruitTreeSource(ItemSource):
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True, **kw_only)
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class HarvestCropSource(ItemSource):
|
||||
add_tags = (ItemTag.CROPSANITY,)
|
||||
|
||||
@@ -61,6 +60,6 @@ class HarvestCropSource(ItemSource):
|
||||
}
|
||||
|
||||
|
||||
@dataclass(frozen=True, **kw_only)
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class ArtifactSpotSource(ItemSource):
|
||||
amount: int
|
||||
|
||||
@@ -7,7 +7,7 @@ id,name,classification,groups,mod_name
|
||||
19,Glittering Boulder Removed,progression,COMMUNITY_REWARD,
|
||||
20,Minecarts Repair,useful,COMMUNITY_REWARD,
|
||||
21,Bus Repair,progression,COMMUNITY_REWARD,
|
||||
22,Progressive Movie Theater,progression,COMMUNITY_REWARD,
|
||||
22,Progressive Movie Theater,"progression,trap",COMMUNITY_REWARD,
|
||||
23,Stardrop,progression,,
|
||||
24,Progressive Backpack,progression,,
|
||||
25,Rusty Sword,filler,"WEAPON,DEPRECATED",
|
||||
|
||||
|
@@ -1,40 +1,39 @@
|
||||
from dataclasses import dataclass
|
||||
from typing import Tuple, Optional
|
||||
|
||||
from .game_item import ItemSource, kw_only, Requirement
|
||||
from .game_item import ItemSource
|
||||
from ..strings.season_names import Season
|
||||
|
||||
ItemPrice = Tuple[int, str]
|
||||
|
||||
|
||||
@dataclass(frozen=True, **kw_only)
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class ShopSource(ItemSource):
|
||||
shop_region: str
|
||||
money_price: Optional[int] = None
|
||||
items_price: Optional[Tuple[ItemPrice, ...]] = None
|
||||
seasons: Tuple[str, ...] = Season.all
|
||||
other_requirements: Tuple[Requirement, ...] = ()
|
||||
|
||||
def __post_init__(self):
|
||||
assert self.money_price is not None or self.items_price is not None, "At least money price or items price need to be defined."
|
||||
assert self.items_price is None or all(isinstance(p, tuple) for p in self.items_price), "Items price should be a tuple."
|
||||
|
||||
|
||||
@dataclass(frozen=True, **kw_only)
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class MysteryBoxSource(ItemSource):
|
||||
amount: int
|
||||
|
||||
|
||||
@dataclass(frozen=True, **kw_only)
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class ArtifactTroveSource(ItemSource):
|
||||
amount: int
|
||||
|
||||
|
||||
@dataclass(frozen=True, **kw_only)
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class PrizeMachineSource(ItemSource):
|
||||
amount: int
|
||||
|
||||
|
||||
@dataclass(frozen=True, **kw_only)
|
||||
@dataclass(frozen=True, kw_only=True)
|
||||
class FishingTreasureChestSource(ItemSource):
|
||||
amount: int
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
from dataclasses import dataclass, field
|
||||
|
||||
from ..data.game_item import kw_only
|
||||
|
||||
|
||||
@dataclass(frozen=True)
|
||||
class Skill:
|
||||
name: str
|
||||
has_mastery: bool = field(**kw_only)
|
||||
has_mastery: bool = field(kw_only=True)
|
||||
|
||||
@@ -138,7 +138,7 @@ This means that, for these specific mods, if you decide to include them in your
|
||||
with the assumption that you will install and play with these mods. The multiworld will contain related items and locations
|
||||
for these mods, the specifics will vary from mod to mod
|
||||
|
||||
[Supported Mods Documentation](https://github.com/agilbert1412/StardewArchipelago/blob/5.x.x/Documentation/Supported%20Mods.md)
|
||||
[Supported Mods Documentation](https://github.com/agilbert1412/StardewArchipelago/blob/6.x.x/Documentation/Supported%20Mods.md)
|
||||
|
||||
List of supported mods:
|
||||
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
- Archipelago from the [Archipelago Releases Page](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
* (Only for the TextClient)
|
||||
- Other Stardew Valley Mods [Nexus Mods](https://www.nexusmods.com/stardewvalley)
|
||||
* There are [supported mods](https://github.com/agilbert1412/StardewArchipelago/blob/5.x.x/Documentation/Supported%20Mods.md)
|
||||
* There are [supported mods](https://github.com/agilbert1412/StardewArchipelago/blob/6.x.x/Documentation/Supported%20Mods.md)
|
||||
that you can add to your yaml to include them with the Archipelago randomization
|
||||
|
||||
* It is **not** recommended to further mod Stardew Valley with unsupported mods, although it is possible to do so.
|
||||
|
||||
@@ -2,6 +2,7 @@ import csv
|
||||
import enum
|
||||
import logging
|
||||
from dataclasses import dataclass, field
|
||||
from functools import reduce
|
||||
from pathlib import Path
|
||||
from random import Random
|
||||
from typing import Dict, List, Protocol, Union, Set, Optional
|
||||
@@ -124,17 +125,14 @@ class StardewItemDeleter(Protocol):
|
||||
|
||||
|
||||
def load_item_csv():
|
||||
try:
|
||||
from importlib.resources import files
|
||||
except ImportError:
|
||||
from importlib_resources import files # noqa
|
||||
from importlib.resources import files
|
||||
|
||||
items = []
|
||||
with files(data).joinpath("items.csv").open() as file:
|
||||
item_reader = csv.DictReader(file)
|
||||
for item in item_reader:
|
||||
id = int(item["id"]) if item["id"] else None
|
||||
classification = ItemClassification[item["classification"]]
|
||||
classification = reduce((lambda a, b: a | b), {ItemClassification[str_classification] for str_classification in item["classification"].split(",")})
|
||||
groups = {Group[group] for group in item["groups"].split(",") if group}
|
||||
mod_name = str(item["mod_name"]) if item["mod_name"] else None
|
||||
items.append(ItemData(id, item["name"], classification, mod_name, groups))
|
||||
|
||||
@@ -130,10 +130,7 @@ class StardewLocationCollector(Protocol):
|
||||
|
||||
|
||||
def load_location_csv() -> List[LocationData]:
|
||||
try:
|
||||
from importlib.resources import files
|
||||
except ImportError:
|
||||
from importlib_resources import files
|
||||
from importlib.resources import files
|
||||
|
||||
with files(data).joinpath("locations.csv").open() as file:
|
||||
reader = csv.DictReader(file)
|
||||
|
||||
@@ -41,9 +41,7 @@ all_random_settings = {
|
||||
Friendsanity.internal_name: "random",
|
||||
FriendsanityHeartSize.internal_name: "random",
|
||||
Booksanity.internal_name: "random",
|
||||
Walnutsanity.internal_name: "random",
|
||||
NumberOfMovementBuffs.internal_name: "random",
|
||||
EnabledFillerBuffs.internal_name: "random",
|
||||
ExcludeGingerIsland.internal_name: "random",
|
||||
TrapItems.internal_name: "random",
|
||||
MultipleDaySleepEnabled.internal_name: "random",
|
||||
|
||||
@@ -1,2 +0,0 @@
|
||||
importlib_resources; python_version <= '3.8'
|
||||
graphlib_backport; python_version <= '3.8'
|
||||
@@ -35,7 +35,7 @@ class TestBaseItemGeneration(SVTestBase):
|
||||
items_to_ignore.extend(baby.name for baby in items.items_by_group[Group.BABY])
|
||||
items_to_ignore.extend(resource_pack.name for resource_pack in items.items_by_group[Group.RESOURCE_PACK])
|
||||
items_to_ignore.append("The Gateway Gazette")
|
||||
progression_items = [item for item in items.all_items if item.classification is ItemClassification.progression and item.name not in items_to_ignore]
|
||||
progression_items = [item for item in items.all_items if item.classification & ItemClassification.progression and item.name not in items_to_ignore]
|
||||
for progression_item in progression_items:
|
||||
with self.subTest(f"{progression_item.name}"):
|
||||
self.assertIn(progression_item.name, all_created_items)
|
||||
@@ -86,7 +86,7 @@ class TestNoGingerIslandItemGeneration(SVTestBase):
|
||||
items_to_ignore.extend(season.name for season in items.items_by_group[Group.WEAPON])
|
||||
items_to_ignore.extend(baby.name for baby in items.items_by_group[Group.BABY])
|
||||
items_to_ignore.append("The Gateway Gazette")
|
||||
progression_items = [item for item in items.all_items if item.classification is ItemClassification.progression and item.name not in items_to_ignore]
|
||||
progression_items = [item for item in items.all_items if item.classification & ItemClassification.progression and item.name not in items_to_ignore]
|
||||
for progression_item in progression_items:
|
||||
with self.subTest(f"{progression_item.name}"):
|
||||
if Group.GINGER_ISLAND in progression_item.groups:
|
||||
|
||||
@@ -306,7 +306,7 @@ class SVTestBase(RuleAssertMixin, WorldTestBase, SVTestCase):
|
||||
|
||||
def create_item(self, item: str) -> StardewItem:
|
||||
created_item = self.world.create_item(item)
|
||||
if created_item.classification == ItemClassification.progression:
|
||||
if created_item.classification & ItemClassification.progression:
|
||||
self.multiworld.worlds[self.player].total_progression_items -= 1
|
||||
return created_item
|
||||
|
||||
|
||||
@@ -75,7 +75,7 @@ class TestBaseItemGeneration(SVTestBase):
|
||||
items_to_ignore.extend(baby.name for baby in items.items_by_group[Group.BABY])
|
||||
items_to_ignore.extend(resource_pack.name for resource_pack in items.items_by_group[Group.RESOURCE_PACK])
|
||||
items_to_ignore.append("The Gateway Gazette")
|
||||
progression_items = [item for item in items.all_items if item.classification is ItemClassification.progression
|
||||
progression_items = [item for item in items.all_items if item.classification & ItemClassification.progression
|
||||
and item.name not in items_to_ignore]
|
||||
for progression_item in progression_items:
|
||||
with self.subTest(f"{progression_item.name}"):
|
||||
@@ -105,7 +105,7 @@ class TestNoGingerIslandModItemGeneration(SVTestBase):
|
||||
items_to_ignore.extend(baby.name for baby in items.items_by_group[Group.BABY])
|
||||
items_to_ignore.extend(resource_pack.name for resource_pack in items.items_by_group[Group.RESOURCE_PACK])
|
||||
items_to_ignore.append("The Gateway Gazette")
|
||||
progression_items = [item for item in items.all_items if item.classification is ItemClassification.progression
|
||||
progression_items = [item for item in items.all_items if item.classification & ItemClassification.progression
|
||||
and item.name not in items_to_ignore]
|
||||
for progression_item in progression_items:
|
||||
with self.subTest(f"{progression_item.name}"):
|
||||
|
||||
@@ -8,5 +8,5 @@ class TestHasProgressionPercent(unittest.TestCase):
|
||||
def test_max_item_amount_is_full_collection(self):
|
||||
# Not caching because it fails too often for some reason
|
||||
with solo_multiworld(world_caching=False) as (multiworld, world):
|
||||
progression_item_count = sum(1 for i in multiworld.get_items() if ItemClassification.progression in i.classification)
|
||||
progression_item_count = sum(1 for i in multiworld.get_items() if i.classification & ItemClassification.progression)
|
||||
self.assertEqual(world.total_progression_items, progression_item_count - 1) # -1 to skip Victory
|
||||
|
||||
@@ -12,8 +12,6 @@ BYTES_TO_REMOVE = 4
|
||||
|
||||
# <function Location.<lambda> at 0x102ca98a0>
|
||||
lambda_regex = re.compile(r"^<function Location\.<lambda> at (.*)>$")
|
||||
# Python 3.10.2\r\n
|
||||
python_version_regex = re.compile(r"^Python (\d+)\.(\d+)\.(\d+)\s*$")
|
||||
|
||||
|
||||
class TestGenerationIsStable(SVTestCase):
|
||||
|
||||
@@ -112,8 +112,7 @@ class AggressiveScanLogic(Choice):
|
||||
|
||||
|
||||
class SubnauticaDeathLink(DeathLink):
|
||||
"""When you die, everyone dies. Of course the reverse is true too.
|
||||
Note: can be toggled via in-game console command "deathlink"."""
|
||||
__doc__ = DeathLink.__doc__ + "\n\n Note: can be toggled via in-game console command \"deathlink\"."
|
||||
|
||||
|
||||
class FillerItemsDistribution(ItemDict):
|
||||
|
||||
@@ -379,6 +379,7 @@ class TimespinnerOptions(PerGameCommonOptions, DeathLinkMixin):
|
||||
cantoran: Cantoran
|
||||
lore_checks: LoreChecks
|
||||
boss_rando: BossRando
|
||||
enemy_rando: EnemyRando
|
||||
damage_rando: DamageRando
|
||||
damage_rando_overrides: DamageRandoOverrides
|
||||
hp_cap: HpCap
|
||||
@@ -445,6 +446,7 @@ class BackwardsCompatiableTimespinnerOptions(TimespinnerOptions):
|
||||
Cantoran: hidden(Cantoran) # type: ignore
|
||||
LoreChecks: hidden(LoreChecks) # type: ignore
|
||||
BossRando: hidden(BossRando) # type: ignore
|
||||
EnemyRando: hidden(EnemyRando) # type: ignore
|
||||
DamageRando: hidden(DamageRando) # type: ignore
|
||||
DamageRandoOverrides: HiddenDamageRandoOverrides
|
||||
HpCap: hidden(HpCap) # type: ignore
|
||||
@@ -516,6 +518,10 @@ class BackwardsCompatiableTimespinnerOptions(TimespinnerOptions):
|
||||
self.boss_rando == BossRando.default:
|
||||
self.boss_rando.value = self.BossRando.value
|
||||
self.has_replaced_options.value = Toggle.option_true
|
||||
if self.EnemyRando != EnemyRando.default and \
|
||||
self.enemy_rando == EnemyRando.default:
|
||||
self.enemy_rando.value = self.EnemyRando.value
|
||||
self.has_replaced_options.value = Toggle.option_true
|
||||
if self.DamageRando != DamageRando.default and \
|
||||
self.damage_rando == DamageRando.default:
|
||||
self.damage_rando.value = self.DamageRando.value
|
||||
|
||||
@@ -98,6 +98,7 @@ class TimespinnerWorld(World):
|
||||
"Cantoran": self.options.cantoran.value,
|
||||
"LoreChecks": self.options.lore_checks.value,
|
||||
"BossRando": self.options.boss_rando.value,
|
||||
"EnemyRando": self.options.enemy_rando.value,
|
||||
"DamageRando": self.options.damage_rando.value,
|
||||
"DamageRandoOverrides": self.options.damage_rando_overrides.value,
|
||||
"HpCap": self.options.hp_cap.value,
|
||||
|
||||
@@ -108,11 +108,15 @@ sword_cave_locations = [
|
||||
]
|
||||
|
||||
food_locations = [
|
||||
"Level 7 Map", "Level 7 Boss", "Level 7 Triforce", "Level 7 Key Drop (Goriyas)",
|
||||
"Level 7 Item (Red Candle)", "Level 7 Map", "Level 7 Boss", "Level 7 Triforce", "Level 7 Key Drop (Goriyas)",
|
||||
"Level 7 Bomb Drop (Moldorms North)", "Level 7 Bomb Drop (Goriyas North)",
|
||||
"Level 7 Bomb Drop (Dodongos)", "Level 7 Rupee Drop (Goriyas North)"
|
||||
]
|
||||
|
||||
gohma_locations = [
|
||||
"Level 6 Boss", "Level 6 Triforce", "Level 8 Item (Magical Key)", "Level 8 Bomb Drop (Darknuts North)"
|
||||
]
|
||||
|
||||
gleeok_locations = [
|
||||
"Level 4 Boss", "Level 4 Triforce", "Level 8 Boss", "Level 8 Triforce"
|
||||
]
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
from worlds.generic.Rules import add_rule
|
||||
from .Locations import food_locations, shop_locations, gleeok_locations
|
||||
from .Locations import food_locations, shop_locations, gleeok_locations, gohma_locations
|
||||
from .ItemPool import dangerous_weapon_locations
|
||||
from .Options import StartingPosition
|
||||
|
||||
@@ -10,13 +10,12 @@ 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):
|
||||
boss = world.get_location(f"Level {level} Boss", player)
|
||||
boss_event = world.get_location(f"Level {level} Boss Status", player)
|
||||
boss = tloz_world.get_location(f"Level {level} Boss")
|
||||
boss_event = tloz_world.get_location(f"Level {level} Boss Status")
|
||||
status = tloz_world.create_event(f"Boss {level} Defeated")
|
||||
boss_event.place_locked_item(status)
|
||||
add_rule(boss_event, lambda state, b=boss: state.can_reach(b, "Location", player))
|
||||
@@ -26,136 +25,131 @@ def set_rules(tloz_world: "TLoZWorld"):
|
||||
for location in level.locations:
|
||||
if options.StartingPosition < StartingPosition.option_dangerous \
|
||||
or location.name not in dangerous_weapon_locations:
|
||||
add_rule(world.get_location(location.name, player),
|
||||
add_rule(tloz_world.get_location(location.name),
|
||||
lambda state: state.has_group("weapons", player))
|
||||
# This part of the loop sets up an expected amount of defense needed for each dungeon
|
||||
if i > 0: # Don't need an extra heart for Level 1
|
||||
add_rule(world.get_location(location.name, player),
|
||||
add_rule(tloz_world.get_location(location.name),
|
||||
lambda state, hearts=i: state.has("Heart Container", player, hearts) or
|
||||
(state.has("Blue Ring", player) and
|
||||
state.has("Heart Container", player, int(hearts / 2))) or
|
||||
(state.has("Red Ring", player) and
|
||||
state.has("Heart Container", player, int(hearts / 4))))
|
||||
if "Pols Voice" in location.name: # This enemy needs specific weapons
|
||||
add_rule(world.get_location(location.name, player),
|
||||
lambda state: state.has_group("swords", player) or state.has("Bow", player))
|
||||
add_rule(tloz_world.get_location(location.name),
|
||||
lambda state: state.has_group("swords", player) or
|
||||
(state.has("Bow", player) and state.has_group("arrows", player)))
|
||||
|
||||
# No requiring anything in a shop until we can farm for money
|
||||
for location in shop_locations:
|
||||
add_rule(world.get_location(location, player),
|
||||
add_rule(tloz_world.get_location(location),
|
||||
lambda state: state.has_group("weapons", player))
|
||||
|
||||
# Everything from 4 on up has dark rooms
|
||||
for level in tloz_world.levels[4:]:
|
||||
for location in level.locations:
|
||||
add_rule(world.get_location(location.name, player),
|
||||
add_rule(tloz_world.get_location(location.name),
|
||||
lambda state: state.has_group("candles", player)
|
||||
or (state.has("Magical Rod", player) and state.has("Book of Magic", player)))
|
||||
|
||||
# Everything from 5 on up has gaps
|
||||
for level in tloz_world.levels[5:]:
|
||||
for location in level.locations:
|
||||
add_rule(world.get_location(location.name, player),
|
||||
add_rule(tloz_world.get_location(location.name),
|
||||
lambda state: state.has("Stepladder", player))
|
||||
|
||||
add_rule(world.get_location("Level 5 Boss", player),
|
||||
# Level 4 Access
|
||||
for location in tloz_world.levels[4].locations:
|
||||
add_rule(tloz_world.get_location(location.name),
|
||||
lambda state: state.has_any(("Raft", "Recorder"), player))
|
||||
|
||||
# Digdogger boss. Rework this once ER happens
|
||||
add_rule(tloz_world.get_location("Level 5 Boss"),
|
||||
lambda state: state.has("Recorder", player))
|
||||
add_rule(tloz_world.get_location("Level 5 Triforce"),
|
||||
lambda state: state.has("Recorder", player))
|
||||
|
||||
add_rule(world.get_location("Level 6 Boss", player),
|
||||
lambda state: state.has("Bow", player) and state.has_group("arrows", player))
|
||||
for location in gohma_locations:
|
||||
if options.ExpandedPool or "Drop" not in location:
|
||||
add_rule(tloz_world.get_location(location),
|
||||
lambda state: state.has("Bow", player) and state.has_group("arrows", player))
|
||||
|
||||
add_rule(world.get_location("Level 7 Item (Red Candle)", player),
|
||||
lambda state: state.has("Recorder", player))
|
||||
add_rule(world.get_location("Level 7 Boss", player),
|
||||
lambda state: state.has("Recorder", 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),
|
||||
lambda state: state.has("Recorder", player))
|
||||
add_rule(world.get_location("Level 7 Rupee Drop (Dodongos)", player),
|
||||
# Recorder Access for Level 7
|
||||
for location in tloz_world.levels[7].locations:
|
||||
add_rule(tloz_world.get_location(location.name),
|
||||
lambda state: state.has("Recorder", player))
|
||||
|
||||
for location in food_locations:
|
||||
if options.ExpandedPool or "Drop" not in location:
|
||||
add_rule(world.get_location(location, player),
|
||||
add_rule(tloz_world.get_location(location),
|
||||
lambda state: state.has("Food", player))
|
||||
|
||||
for location in gleeok_locations:
|
||||
add_rule(world.get_location(location, player),
|
||||
add_rule(tloz_world.get_location(location),
|
||||
lambda state: state.has_group("swords", player) or state.has("Magical Rod", player))
|
||||
|
||||
# Candle access for Level 8
|
||||
for location in tloz_world.levels[8].locations:
|
||||
add_rule(world.get_location(location.name, player),
|
||||
add_rule(tloz_world.get_location(location.name),
|
||||
lambda state: state.has_group("candles", player))
|
||||
|
||||
add_rule(world.get_location("Level 8 Item (Magical Key)", player),
|
||||
add_rule(tloz_world.get_location("Level 8 Item (Magical Key)"),
|
||||
lambda state: state.has("Bow", player) and state.has_group("arrows", player))
|
||||
if options.ExpandedPool:
|
||||
add_rule(world.get_location("Level 8 Bomb Drop (Darknuts North)", player),
|
||||
add_rule(tloz_world.get_location("Level 8 Bomb Drop (Darknuts North)"),
|
||||
lambda state: state.has("Bow", player) and state.has_group("arrows", player))
|
||||
|
||||
for location in tloz_world.levels[9].locations:
|
||||
add_rule(world.get_location(location.name, player),
|
||||
add_rule(tloz_world.get_location(location.name),
|
||||
lambda state: state.has("Triforce Fragment", player, 8) and
|
||||
state.has_group("swords", player))
|
||||
|
||||
# Yes we are looping this range again for Triforce locations. No I can't add it to the boss event loop
|
||||
for level in range(1, 9):
|
||||
add_rule(world.get_location(f"Level {level} Triforce", player),
|
||||
add_rule(tloz_world.get_location(f"Level {level} Triforce"),
|
||||
lambda state, l=level: state.has(f"Boss {l} Defeated", player))
|
||||
|
||||
# Sword, raft, and ladder spots
|
||||
add_rule(world.get_location("White Sword Pond", player),
|
||||
add_rule(tloz_world.get_location("White Sword Pond"),
|
||||
lambda state: state.has("Heart Container", player, 2))
|
||||
add_rule(world.get_location("Magical Sword Grave", player),
|
||||
add_rule(tloz_world.get_location("Magical Sword Grave"),
|
||||
lambda state: state.has("Heart Container", player, 9))
|
||||
|
||||
stepladder_locations = ["Ocean Heart Container", "Level 4 Triforce", "Level 4 Boss", "Level 4 Map"]
|
||||
stepladder_locations_expanded = ["Level 4 Key Drop (Keese North)"]
|
||||
for location in stepladder_locations:
|
||||
add_rule(world.get_location(location, player),
|
||||
add_rule(tloz_world.get_location(location),
|
||||
lambda state: state.has("Stepladder", player))
|
||||
if options.ExpandedPool:
|
||||
for location in stepladder_locations_expanded:
|
||||
add_rule(world.get_location(location, player),
|
||||
add_rule(tloz_world.get_location(location),
|
||||
lambda state: state.has("Stepladder", player))
|
||||
|
||||
# Don't allow Take Any Items until we can actually get in one
|
||||
if options.ExpandedPool:
|
||||
add_rule(world.get_location("Take Any Item Left", player),
|
||||
add_rule(tloz_world.get_location("Take Any Item Left"),
|
||||
lambda state: state.has_group("candles", player) or
|
||||
state.has("Raft", player))
|
||||
add_rule(world.get_location("Take Any Item Middle", player),
|
||||
add_rule(tloz_world.get_location("Take Any Item Middle"),
|
||||
lambda state: state.has_group("candles", player) or
|
||||
state.has("Raft", player))
|
||||
add_rule(world.get_location("Take Any Item Right", player),
|
||||
add_rule(tloz_world.get_location("Take Any Item Right"),
|
||||
lambda state: state.has_group("candles", player) or
|
||||
state.has("Raft", player))
|
||||
for location in tloz_world.levels[4].locations:
|
||||
add_rule(world.get_location(location.name, player),
|
||||
lambda state: state.has("Raft", player) or state.has("Recorder", player))
|
||||
for location in tloz_world.levels[7].locations:
|
||||
add_rule(world.get_location(location.name, player),
|
||||
lambda state: state.has("Recorder", player))
|
||||
for location in tloz_world.levels[8].locations:
|
||||
add_rule(world.get_location(location.name, player),
|
||||
lambda state: state.has("Bow", player))
|
||||
|
||||
add_rule(world.get_location("Potion Shop Item Left", player),
|
||||
add_rule(tloz_world.get_location("Potion Shop Item Left"),
|
||||
lambda state: state.has("Letter", player))
|
||||
add_rule(world.get_location("Potion Shop Item Middle", player),
|
||||
add_rule(tloz_world.get_location("Potion Shop Item Middle"),
|
||||
lambda state: state.has("Letter", player))
|
||||
add_rule(world.get_location("Potion Shop Item Right", player),
|
||||
add_rule(tloz_world.get_location("Potion Shop Item Right"),
|
||||
lambda state: state.has("Letter", player))
|
||||
|
||||
add_rule(world.get_location("Shield Shop Item Left", player),
|
||||
add_rule(tloz_world.get_location("Shield Shop Item Left"),
|
||||
lambda state: state.has_group("candles", player) or
|
||||
state.has("Bomb", player))
|
||||
add_rule(world.get_location("Shield Shop Item Middle", player),
|
||||
add_rule(tloz_world.get_location("Shield Shop Item Middle"),
|
||||
lambda state: state.has_group("candles", player) or
|
||||
state.has("Bomb", player))
|
||||
add_rule(world.get_location("Shield Shop Item Right", player),
|
||||
add_rule(tloz_world.get_location("Shield Shop Item Right"),
|
||||
lambda state: state.has_group("candles", player) or
|
||||
state.has("Bomb", player))
|
||||
state.has("Bomb", player))
|
||||
|
||||
@@ -83,6 +83,11 @@ class TunicWorld(World):
|
||||
shop_num: int = 1 # need to make it so that you can walk out of shops, but also that they aren't all connected
|
||||
er_regions: Dict[str, RegionInfo] # absolutely needed so outlet regions work
|
||||
|
||||
# so we only loop the multiworld locations once
|
||||
# if these are locations instead of their info, it gives a memory leak error
|
||||
item_link_locations: Dict[int, Dict[str, List[Tuple[int, str]]]] = {}
|
||||
player_item_link_locations: Dict[str, List[Location]]
|
||||
|
||||
def generate_early(self) -> None:
|
||||
if self.options.logic_rules >= LogicRules.option_no_major_glitches:
|
||||
self.options.laurels_zips.value = LaurelsZips.option_true
|
||||
@@ -387,6 +392,18 @@ class TunicWorld(World):
|
||||
if hint_text:
|
||||
hint_data[self.player][location.address] = hint_text
|
||||
|
||||
def get_real_location(self, location: Location) -> Tuple[str, int]:
|
||||
# if it's not in a group, it's not in an item link
|
||||
if location.player not in self.multiworld.groups or not location.item:
|
||||
return location.name, location.player
|
||||
try:
|
||||
loc = self.player_item_link_locations[location.item.name].pop()
|
||||
return loc.name, loc.player
|
||||
except IndexError:
|
||||
warning(f"TUNIC: Failed to parse item location for in-game hints for {self.player_name}. "
|
||||
f"Using a potentially incorrect location name instead.")
|
||||
return location.name, location.player
|
||||
|
||||
def fill_slot_data(self) -> Dict[str, Any]:
|
||||
slot_data: Dict[str, Any] = {
|
||||
"seed": self.random.randint(0, 2147483647),
|
||||
@@ -412,12 +429,35 @@ class TunicWorld(World):
|
||||
"disable_local_spoiler": int(self.settings.disable_local_spoiler or self.multiworld.is_race),
|
||||
}
|
||||
|
||||
# this would be in a stage if there was an appropriate stage for it
|
||||
self.player_item_link_locations = {}
|
||||
groups = self.multiworld.get_player_groups(self.player)
|
||||
# checking if groups so that this doesn't run if the player isn't in a group
|
||||
if groups:
|
||||
if not self.item_link_locations:
|
||||
tunic_worlds: Tuple[TunicWorld] = self.multiworld.get_game_worlds("TUNIC")
|
||||
# figure out our groups and the items in them
|
||||
for tunic in tunic_worlds:
|
||||
for group in self.multiworld.get_player_groups(tunic.player):
|
||||
self.item_link_locations.setdefault(group, {})
|
||||
for location in self.multiworld.get_locations():
|
||||
if location.item and location.item.player in self.item_link_locations.keys():
|
||||
(self.item_link_locations[location.item.player].setdefault(location.item.name, [])
|
||||
.append((location.player, location.name)))
|
||||
|
||||
# if item links are on, set up the player's personal item link locations, so we can pop them as needed
|
||||
for group, item_links in self.item_link_locations.items():
|
||||
if group in groups:
|
||||
for item_name, locs in item_links.items():
|
||||
self.player_item_link_locations[item_name] = \
|
||||
[self.multiworld.get_location(location_name, player) for player, location_name in locs]
|
||||
|
||||
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])
|
||||
slot_data[tunic_item.name].extend(self.get_real_location(tunic_item.location))
|
||||
|
||||
for start_item in self.options.start_inventory_from_pool:
|
||||
if start_item in slot_data_item_names:
|
||||
@@ -436,7 +476,7 @@ class TunicWorld(World):
|
||||
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])
|
||||
slot_data[item].extend(self.get_real_location(item_location))
|
||||
|
||||
return slot_data
|
||||
|
||||
|
||||
@@ -807,7 +807,7 @@ traversal_requirements: Dict[str, Dict[str, List[List[str]]]] = {
|
||||
[],
|
||||
# drop a rudeling, icebolt or ice bomb
|
||||
"Overworld to West Garden from Furnace":
|
||||
[["IG3"]],
|
||||
[["IG3"], ["LS1"]],
|
||||
},
|
||||
"East Overworld": {
|
||||
"Above Ruined Passage":
|
||||
|
||||
@@ -501,9 +501,11 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
||||
regions["Dark Tomb Upper"].connect(
|
||||
connecting_region=regions["Dark Tomb Entry Point"])
|
||||
|
||||
# ice grapple through the wall, get the little secret sound to trigger
|
||||
regions["Dark Tomb Upper"].connect(
|
||||
connecting_region=regions["Dark Tomb Main"],
|
||||
rule=lambda state: has_ladder("Ladder in Dark Tomb", state, world))
|
||||
rule=lambda state: has_ladder("Ladder in Dark Tomb", state, world)
|
||||
or has_ice_grapple_logic(False, IceGrappling.option_hard, state, world))
|
||||
regions["Dark Tomb Main"].connect(
|
||||
connecting_region=regions["Dark Tomb Upper"],
|
||||
rule=lambda state: has_ladder("Ladder in Dark Tomb", state, world))
|
||||
@@ -779,12 +781,10 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
||||
|
||||
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, IceGrappling.option_easy, state, world))
|
||||
|
||||
# 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"],
|
||||
rule=lambda state: state.has_all({"Activate Eastern Vault West Fuses",
|
||||
@@ -807,7 +807,6 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
||||
regions["Fortress Hero's Grave Region"].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, IceGrappling.option_easy, state, world))
|
||||
@@ -1139,6 +1138,9 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
||||
for portal_dest in region_info.portals:
|
||||
ls_connect(ladder_region, "Overworld Redux, " + portal_dest)
|
||||
|
||||
# convenient staircase means this one is easy difficulty, even though there's an elevation change
|
||||
ls_connect("LS Elev 0", "Overworld Redux, Furnace_gyro_west")
|
||||
|
||||
# connect ls elevation regions to regions where you can get an enemy to knock you down, also well rail
|
||||
if options.ladder_storage >= LadderStorage.option_medium:
|
||||
for ladder_region, region_info in ow_ladder_groups.items():
|
||||
@@ -1154,6 +1156,7 @@ def set_er_region_rules(world: "TunicWorld", regions: Dict[str, Region], portal_
|
||||
if options.ladder_storage >= LadderStorage.option_hard:
|
||||
ls_connect("LS Elev 1", "Overworld Redux, EastFiligreeCache_")
|
||||
ls_connect("LS Elev 2", "Overworld Redux, Town_FiligreeRoom_")
|
||||
ls_connect("LS Elev 2", "Overworld Redux, Ruins Passage_west")
|
||||
ls_connect("LS Elev 3", "Overworld Redux, Overworld Interiors_house")
|
||||
ls_connect("LS Elev 5", "Overworld Redux, Temple_main")
|
||||
|
||||
|
||||
@@ -17,7 +17,7 @@ ow_ladder_groups: Dict[str, OWLadderInfo] = {
|
||||
["Overworld Beach"]),
|
||||
# also the east filigree room
|
||||
"LS Elev 1": OWLadderInfo({"Ladders near Weathervane", "Ladders in Overworld Town", "Ladder to Swamp"},
|
||||
["Furnace_gyro_lower", "Swamp Redux 2_wall"],
|
||||
["Furnace_gyro_lower", "Furnace_gyro_west", "Swamp Redux 2_wall"],
|
||||
["Overworld Tunnel Turret"]),
|
||||
# also the fountain filigree room and ruined passage door
|
||||
"LS Elev 2": OWLadderInfo({"Ladders near Weathervane", "Ladders to West Bell"},
|
||||
|
||||
@@ -80,7 +80,7 @@ class WitnessWorld(World):
|
||||
|
||||
def _get_slot_data(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"seed": self.random.randrange(0, 1000000),
|
||||
"seed": self.options.puzzle_randomization_seed.value,
|
||||
"victory_location": int(self.player_logic.VICTORY_LOCATION, 16),
|
||||
"panelhex_to_id": self.player_locations.CHECK_PANELHEX_TO_ID,
|
||||
"item_id_to_door_hexes": static_witness_items.get_item_to_door_mappings(),
|
||||
|
||||
@@ -401,6 +401,17 @@ class DeathLinkAmnesty(Range):
|
||||
default = 1
|
||||
|
||||
|
||||
class PuzzleRandomizationSeed(Range):
|
||||
"""
|
||||
Sigma Rando, which is the basis for all puzzle randomization in this randomizer, uses a seed from 1 to 9999999 for the puzzle randomization.
|
||||
This option lets you set this seed yourself.
|
||||
"""
|
||||
display_name = "Puzzle Randomization Seed"
|
||||
range_start = 1
|
||||
range_end = 9999999
|
||||
default = "random"
|
||||
|
||||
|
||||
@dataclass
|
||||
class TheWitnessOptions(PerGameCommonOptions):
|
||||
puzzle_randomization: PuzzleRandomization
|
||||
@@ -435,6 +446,7 @@ class TheWitnessOptions(PerGameCommonOptions):
|
||||
laser_hints: LaserHints
|
||||
death_link: DeathLink
|
||||
death_link_amnesty: DeathLinkAmnesty
|
||||
puzzle_randomization_seed: PuzzleRandomizationSeed
|
||||
shuffle_dog: ShuffleDog
|
||||
|
||||
|
||||
@@ -445,7 +457,7 @@ witness_option_groups = [
|
||||
MountainLasers,
|
||||
ChallengeLasers,
|
||||
]),
|
||||
OptionGroup("Panel Hunt Settings", [
|
||||
OptionGroup("Panel Hunt Options", [
|
||||
PanelHuntRequiredPercentage,
|
||||
PanelHuntTotal,
|
||||
PanelHuntPostgame,
|
||||
@@ -483,6 +495,7 @@ witness_option_groups = [
|
||||
ElevatorsComeToYou,
|
||||
DeathLink,
|
||||
DeathLinkAmnesty,
|
||||
PuzzleRandomizationSeed,
|
||||
]),
|
||||
OptionGroup("Silly Options", [
|
||||
ShuffleDog,
|
||||
|
||||
@@ -2,7 +2,7 @@
|
||||
Defines progression, junk and event items for The Witness
|
||||
"""
|
||||
import copy
|
||||
from typing import TYPE_CHECKING, Dict, List, Set, cast
|
||||
from typing import TYPE_CHECKING, Dict, List, Set
|
||||
|
||||
from BaseClasses import Item, ItemClassification, MultiWorld
|
||||
|
||||
|
||||
@@ -1,7 +1,6 @@
|
||||
from collections import Counter
|
||||
from dataclasses import dataclass
|
||||
from typing import ClassVar, Dict, Literal, Tuple
|
||||
from typing_extensions import TypeGuard # remove when Python >= 3.10
|
||||
from typing import ClassVar, Dict, Literal, Tuple, TypeGuard
|
||||
|
||||
from Options import Choice, DefaultOnToggle, NamedRange, OptionGroup, PerGameCommonOptions, Range, Removed, Toggle
|
||||
|
||||
@@ -233,6 +232,7 @@ class ZillionSkill(Range):
|
||||
range_start = 0
|
||||
range_end = 5
|
||||
default = 2
|
||||
display_name = "skill"
|
||||
|
||||
|
||||
class ZillionStartingCards(NamedRange):
|
||||
|
||||
Reference in New Issue
Block a user