mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-23 04:33:24 -07:00
Compare commits
89 Commits
NewSoupVi-
...
NewSoupVi-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
453d89460f | ||
|
|
28889e58aa | ||
|
|
f3c76399e0 | ||
|
|
8384a23fe2 | ||
|
|
bcd7d62d0b | ||
|
|
703f5a22fd | ||
|
|
1ee8e339af | ||
|
|
dffde64079 | ||
|
|
17bc184e28 | ||
|
|
0ba9ee0695 | ||
|
|
c40214e20f | ||
|
|
a3aac3d737 | ||
|
|
7bbe62019a | ||
|
|
b898b9d9e6 | ||
|
|
b217372fea | ||
|
|
b2d2c8e596 | ||
|
|
68e37b8f9a | ||
|
|
fa2d7797f4 | ||
|
|
1885dab066 | ||
|
|
9425f5b772 | ||
|
|
83ed3c8b50 | ||
|
|
f4690e296d | ||
|
|
68c350b4c0 | ||
|
|
da0207f5cb | ||
|
|
2455f1158f | ||
|
|
1031fc4923 | ||
|
|
6beaacb905 | ||
|
|
c46ee7c420 | ||
|
|
227f0bce3d | ||
|
|
611e1c2b19 | ||
|
|
5f974b7457 | ||
|
|
3ef35105c8 | ||
|
|
ec768a2e89 | ||
|
|
b580d3c25a | ||
|
|
ce14f190fb | ||
|
|
4e3da005d4 | ||
|
|
0d9967e8d8 | ||
|
|
2624a0a7ea | ||
|
|
8755d5cbc0 | ||
|
|
abb6d7fbdb | ||
|
|
fc04192c99 | ||
|
|
d4110d3b2a | ||
|
|
05c1751d29 | ||
|
|
6ad042b349 | ||
|
|
e52d8b4dbd | ||
|
|
f288e3469c | ||
|
|
5bb87c6da5 | ||
|
|
03768a5f90 | ||
|
|
a84366368f | ||
|
|
29e6a10e42 | ||
|
|
febd280fba | ||
|
|
73964b374c | ||
|
|
bad6a4b211 | ||
|
|
57d3c52df9 | ||
|
|
d309de2557 | ||
|
|
d5d56ede8b | ||
|
|
6613c29652 | ||
|
|
1a6de25ab6 | ||
|
|
b62c1364a9 | ||
|
|
b59162737d | ||
|
|
543dcb27d8 | ||
|
|
22941168cd | ||
|
|
33dc845de8 | ||
|
|
be0f23beb3 | ||
|
|
b76f2163a4 | ||
|
|
04aa471526 | ||
|
|
b756a67c2a | ||
|
|
a76ee010eb | ||
|
|
eb1fef1f92 | ||
|
|
e498cc7d48 | ||
|
|
a26abe079e | ||
|
|
199b6bdabb | ||
|
|
e4bc7bd1cd | ||
|
|
20651df307 | ||
|
|
f857933748 | ||
|
|
efe2b7c539 | ||
|
|
e090153d93 | ||
|
|
5088b02bfe | ||
|
|
57a716b57a | ||
|
|
1b51714f3b | ||
|
|
cb3d35faf9 | ||
|
|
a0c83b4854 | ||
|
|
1b3ee0e94f | ||
|
|
552a6e7f1c | ||
|
|
38bfb1087b | ||
|
|
2dc55873f0 | ||
|
|
4b1898bfaf | ||
|
|
125bf6f270 | ||
|
|
1873c52aa6 |
29
.github/workflows/build.yml
vendored
29
.github/workflows/build.yml
vendored
@@ -21,12 +21,17 @@ env:
|
|||||||
ENEMIZER_VERSION: 7.1
|
ENEMIZER_VERSION: 7.1
|
||||||
APPIMAGETOOL_VERSION: 13
|
APPIMAGETOOL_VERSION: 13
|
||||||
|
|
||||||
|
permissions: # permissions required for attestation
|
||||||
|
id-token: 'write'
|
||||||
|
attestations: 'write'
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
# build-release-macos: # LF volunteer
|
# build-release-macos: # LF volunteer
|
||||||
|
|
||||||
build-win: # RCs will still be built and signed by hand
|
build-win: # RCs and releases may still be built and signed by hand
|
||||||
runs-on: windows-latest
|
runs-on: windows-latest
|
||||||
steps:
|
steps:
|
||||||
|
# - copy code below to release.yml -
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
- name: Install python
|
- name: Install python
|
||||||
uses: actions/setup-python@v5
|
uses: actions/setup-python@v5
|
||||||
@@ -65,6 +70,18 @@ jobs:
|
|||||||
$contents = Get-ChildItem -Path setups/*.exe -Force -Recurse
|
$contents = Get-ChildItem -Path setups/*.exe -Force -Recurse
|
||||||
$SETUP_NAME=$contents[0].Name
|
$SETUP_NAME=$contents[0].Name
|
||||||
echo "SETUP_NAME=$SETUP_NAME" >> $Env:GITHUB_ENV
|
echo "SETUP_NAME=$SETUP_NAME" >> $Env:GITHUB_ENV
|
||||||
|
# - copy code above to release.yml -
|
||||||
|
- name: Attest Build
|
||||||
|
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||||
|
uses: actions/attest-build-provenance@v2
|
||||||
|
with:
|
||||||
|
subject-path: |
|
||||||
|
build/exe.*/ArchipelagoLauncher.exe
|
||||||
|
build/exe.*/ArchipelagoLauncherDebug.exe
|
||||||
|
build/exe.*/ArchipelagoGenerate.exe
|
||||||
|
build/exe.*/ArchipelagoServer.exe
|
||||||
|
dist/${{ env.ZIP_NAME }}
|
||||||
|
setups/${{ env.SETUP_NAME }}
|
||||||
- name: Check build loads expected worlds
|
- name: Check build loads expected worlds
|
||||||
shell: bash
|
shell: bash
|
||||||
run: |
|
run: |
|
||||||
@@ -142,6 +159,16 @@ jobs:
|
|||||||
echo "APPIMAGE_NAME=$APPIMAGE_NAME" >> $GITHUB_ENV
|
echo "APPIMAGE_NAME=$APPIMAGE_NAME" >> $GITHUB_ENV
|
||||||
echo "TAR_NAME=$TAR_NAME" >> $GITHUB_ENV
|
echo "TAR_NAME=$TAR_NAME" >> $GITHUB_ENV
|
||||||
# - copy code above to release.yml -
|
# - copy code above to release.yml -
|
||||||
|
- name: Attest Build
|
||||||
|
if: ${{ github.event_name == 'workflow_dispatch' }}
|
||||||
|
uses: actions/attest-build-provenance@v2
|
||||||
|
with:
|
||||||
|
subject-path: |
|
||||||
|
build/exe.*/ArchipelagoLauncher
|
||||||
|
build/exe.*/ArchipelagoGenerate
|
||||||
|
build/exe.*/ArchipelagoServer
|
||||||
|
dist/${{ env.APPIMAGE_NAME }}*
|
||||||
|
dist/${{ env.TAR_NAME }}
|
||||||
- name: Build Again
|
- name: Build Again
|
||||||
run: |
|
run: |
|
||||||
source venv/bin/activate
|
source venv/bin/activate
|
||||||
|
|||||||
83
.github/workflows/release.yml
vendored
83
.github/workflows/release.yml
vendored
@@ -11,6 +11,11 @@ env:
|
|||||||
ENEMIZER_VERSION: 7.1
|
ENEMIZER_VERSION: 7.1
|
||||||
APPIMAGETOOL_VERSION: 13
|
APPIMAGETOOL_VERSION: 13
|
||||||
|
|
||||||
|
permissions: # permissions required for attestation
|
||||||
|
id-token: 'write'
|
||||||
|
attestations: 'write'
|
||||||
|
contents: 'write' # additionally required for release
|
||||||
|
|
||||||
jobs:
|
jobs:
|
||||||
create-release:
|
create-release:
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
@@ -26,11 +31,79 @@ jobs:
|
|||||||
env:
|
env:
|
||||||
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
# build-release-windows: # this is done by hand because of signing
|
|
||||||
# build-release-macos: # LF volunteer
|
# build-release-macos: # LF volunteer
|
||||||
|
|
||||||
|
build-release-win:
|
||||||
|
runs-on: windows-latest
|
||||||
|
if: ${{ true }} # change to false to skip if release is built by hand
|
||||||
|
needs: create-release
|
||||||
|
steps:
|
||||||
|
- name: Set env
|
||||||
|
shell: bash
|
||||||
|
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||||
|
# - code below copied from build.yml -
|
||||||
|
- uses: actions/checkout@v4
|
||||||
|
- name: Install python
|
||||||
|
uses: actions/setup-python@v5
|
||||||
|
with:
|
||||||
|
python-version: '~3.12.7'
|
||||||
|
check-latest: true
|
||||||
|
- 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
|
||||||
|
Expand-Archive -Path enemizer.zip -DestinationPath EnemizerCLI -Force
|
||||||
|
choco install innosetup --version=6.2.2 --allow-downgrade
|
||||||
|
- name: Build
|
||||||
|
run: |
|
||||||
|
python -m pip install --upgrade pip
|
||||||
|
python setup.py build_exe --yes
|
||||||
|
if ( $? -eq $false ) {
|
||||||
|
Write-Error "setup.py failed!"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
$NAME="$(ls build | Select-String -Pattern 'exe')".Split('.',2)[1]
|
||||||
|
$ZIP_NAME="Archipelago_$NAME.7z"
|
||||||
|
echo "$NAME -> $ZIP_NAME"
|
||||||
|
echo "ZIP_NAME=$ZIP_NAME" >> $Env:GITHUB_ENV
|
||||||
|
New-Item -Path dist -ItemType Directory -Force
|
||||||
|
cd build
|
||||||
|
Rename-Item "exe.$NAME" Archipelago
|
||||||
|
7z a -mx=9 -mhe=on -ms "../dist/$ZIP_NAME" Archipelago
|
||||||
|
Rename-Item Archipelago "exe.$NAME" # inno_setup.iss expects the original name
|
||||||
|
- name: Build Setup
|
||||||
|
run: |
|
||||||
|
& "${env:ProgramFiles(x86)}\Inno Setup 6\iscc.exe" inno_setup.iss /DNO_SIGNTOOL
|
||||||
|
if ( $? -eq $false ) {
|
||||||
|
Write-Error "Building setup failed!"
|
||||||
|
exit 1
|
||||||
|
}
|
||||||
|
$contents = Get-ChildItem -Path setups/*.exe -Force -Recurse
|
||||||
|
$SETUP_NAME=$contents[0].Name
|
||||||
|
echo "SETUP_NAME=$SETUP_NAME" >> $Env:GITHUB_ENV
|
||||||
|
# - code above copied from build.yml -
|
||||||
|
- name: Attest Build
|
||||||
|
uses: actions/attest-build-provenance@v2
|
||||||
|
with:
|
||||||
|
subject-path: |
|
||||||
|
build/exe.*/ArchipelagoLauncher.exe
|
||||||
|
build/exe.*/ArchipelagoLauncherDebug.exe
|
||||||
|
build/exe.*/ArchipelagoGenerate.exe
|
||||||
|
build/exe.*/ArchipelagoServer.exe
|
||||||
|
setups/*
|
||||||
|
- name: Add to Release
|
||||||
|
uses: softprops/action-gh-release@975c1b265e11dd76618af1c374e7981f9a6ff44a
|
||||||
|
with:
|
||||||
|
draft: true # see above
|
||||||
|
prerelease: false
|
||||||
|
name: Archipelago ${{ env.RELEASE_VERSION }}
|
||||||
|
files: |
|
||||||
|
setups/*
|
||||||
|
env:
|
||||||
|
GITHUB_TOKEN: ${{ secrets.GITHUB_TOKEN }}
|
||||||
|
|
||||||
build-release-ubuntu2204:
|
build-release-ubuntu2204:
|
||||||
runs-on: ubuntu-22.04
|
runs-on: ubuntu-22.04
|
||||||
|
needs: create-release
|
||||||
steps:
|
steps:
|
||||||
- name: Set env
|
- name: Set env
|
||||||
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
run: echo "RELEASE_VERSION=${GITHUB_REF#refs/*/}" >> $GITHUB_ENV
|
||||||
@@ -74,6 +147,14 @@ jobs:
|
|||||||
echo "APPIMAGE_NAME=$APPIMAGE_NAME" >> $GITHUB_ENV
|
echo "APPIMAGE_NAME=$APPIMAGE_NAME" >> $GITHUB_ENV
|
||||||
echo "TAR_NAME=$TAR_NAME" >> $GITHUB_ENV
|
echo "TAR_NAME=$TAR_NAME" >> $GITHUB_ENV
|
||||||
# - code above copied from build.yml -
|
# - code above copied from build.yml -
|
||||||
|
- name: Attest Build
|
||||||
|
uses: actions/attest-build-provenance@v2
|
||||||
|
with:
|
||||||
|
subject-path: |
|
||||||
|
build/exe.*/ArchipelagoLauncher
|
||||||
|
build/exe.*/ArchipelagoGenerate
|
||||||
|
build/exe.*/ArchipelagoServer
|
||||||
|
dist/*
|
||||||
- name: Add to Release
|
- name: Add to Release
|
||||||
uses: softprops/action-gh-release@975c1b265e11dd76618af1c374e7981f9a6ff44a
|
uses: softprops/action-gh-release@975c1b265e11dd76618af1c374e7981f9a6ff44a
|
||||||
with:
|
with:
|
||||||
|
|||||||
@@ -223,7 +223,7 @@ class MultiWorld():
|
|||||||
AutoWorld.AutoWorldRegister.world_types[self.game[player]].options_dataclass.type_hints}
|
AutoWorld.AutoWorldRegister.world_types[self.game[player]].options_dataclass.type_hints}
|
||||||
for option_key in all_keys:
|
for option_key in all_keys:
|
||||||
option = Utils.DeprecateDict(f"Getting options from multiworld is now deprecated. "
|
option = Utils.DeprecateDict(f"Getting options from multiworld is now deprecated. "
|
||||||
f"Please use `self.options.{option_key}` instead.")
|
f"Please use `self.options.{option_key}` instead.", True)
|
||||||
option.update(getattr(args, option_key, {}))
|
option.update(getattr(args, option_key, {}))
|
||||||
setattr(self, option_key, option)
|
setattr(self, option_key, option)
|
||||||
|
|
||||||
@@ -1022,9 +1022,6 @@ class Entrance:
|
|||||||
connected_region: Optional[Region] = None
|
connected_region: Optional[Region] = None
|
||||||
randomization_group: int
|
randomization_group: int
|
||||||
randomization_type: EntranceType
|
randomization_type: EntranceType
|
||||||
# LttP specific, TODO: should make a LttPEntrance
|
|
||||||
addresses = None
|
|
||||||
target = None
|
|
||||||
|
|
||||||
def __init__(self, player: int, name: str = "", parent: Optional[Region] = None,
|
def __init__(self, player: int, name: str = "", parent: Optional[Region] = None,
|
||||||
randomization_group: int = 0, randomization_type: EntranceType = EntranceType.ONE_WAY) -> None:
|
randomization_group: int = 0, randomization_type: EntranceType = EntranceType.ONE_WAY) -> None:
|
||||||
@@ -1043,10 +1040,8 @@ class Entrance:
|
|||||||
|
|
||||||
return False
|
return False
|
||||||
|
|
||||||
def connect(self, region: Region, addresses: Any = None, target: Any = None) -> None:
|
def connect(self, region: Region) -> None:
|
||||||
self.connected_region = region
|
self.connected_region = region
|
||||||
self.target = target
|
|
||||||
self.addresses = addresses
|
|
||||||
region.entrances.append(self)
|
region.entrances.append(self)
|
||||||
|
|
||||||
def is_valid_source_transition(self, er_state: "ERPlacementState") -> bool:
|
def is_valid_source_transition(self, er_state: "ERPlacementState") -> bool:
|
||||||
@@ -1203,6 +1198,48 @@ class Region:
|
|||||||
for location, address in locations.items():
|
for location, address in locations.items():
|
||||||
self.locations.append(location_type(self.player, location, address, self))
|
self.locations.append(location_type(self.player, location, address, self))
|
||||||
|
|
||||||
|
def add_event(
|
||||||
|
self,
|
||||||
|
location_name: str,
|
||||||
|
item_name: str | None = None,
|
||||||
|
rule: Callable[[CollectionState], bool] | None = None,
|
||||||
|
location_type: type[Location] | None = None,
|
||||||
|
item_type: type[Item] | None = None,
|
||||||
|
show_in_spoiler: bool = True,
|
||||||
|
) -> Item:
|
||||||
|
"""
|
||||||
|
Adds an event location/item pair to the region.
|
||||||
|
|
||||||
|
:param location_name: Name for the event location.
|
||||||
|
:param item_name: Name for the event item. If not provided, defaults to location_name.
|
||||||
|
:param rule: Callable to determine access for this event location within its region.
|
||||||
|
:param location_type: Location class to create the event location with. Defaults to BaseClasses.Location.
|
||||||
|
:param item_type: Item class to create the event item with. Defaults to BaseClasses.Item.
|
||||||
|
:param show_in_spoiler: Will be passed along to the created event Location's show_in_spoiler attribute.
|
||||||
|
:return: The created Event Item
|
||||||
|
"""
|
||||||
|
if location_type is None:
|
||||||
|
location_type = Location
|
||||||
|
|
||||||
|
if item_name is None:
|
||||||
|
item_name = location_name
|
||||||
|
|
||||||
|
if item_type is None:
|
||||||
|
item_type = Item
|
||||||
|
|
||||||
|
event_location = location_type(self.player, location_name, None, self)
|
||||||
|
event_location.show_in_spoiler = show_in_spoiler
|
||||||
|
if rule is not None:
|
||||||
|
event_location.access_rule = rule
|
||||||
|
|
||||||
|
event_item = item_type(item_name, ItemClassification.progression, None, self.player)
|
||||||
|
|
||||||
|
event_location.place_locked_item(event_item)
|
||||||
|
|
||||||
|
self.locations.append(event_location)
|
||||||
|
|
||||||
|
return event_item
|
||||||
|
|
||||||
def connect(self, connecting_region: Region, name: Optional[str] = None,
|
def connect(self, connecting_region: Region, name: Optional[str] = None,
|
||||||
rule: Optional[Callable[[CollectionState], bool]] = None) -> Entrance:
|
rule: Optional[Callable[[CollectionState], bool]] = None) -> Entrance:
|
||||||
"""
|
"""
|
||||||
|
|||||||
@@ -196,25 +196,11 @@ class CommonContext:
|
|||||||
self.lookup_type: typing.Literal["item", "location"] = lookup_type
|
self.lookup_type: typing.Literal["item", "location"] = lookup_type
|
||||||
self._unknown_item: typing.Callable[[int], str] = lambda key: f"Unknown {lookup_type} (ID: {key})"
|
self._unknown_item: typing.Callable[[int], str] = lambda key: f"Unknown {lookup_type} (ID: {key})"
|
||||||
self._archipelago_lookup: typing.Dict[int, str] = {}
|
self._archipelago_lookup: typing.Dict[int, str] = {}
|
||||||
self._flat_store: typing.Dict[int, str] = Utils.KeyedDefaultDict(self._unknown_item)
|
|
||||||
self._game_store: typing.Dict[str, typing.ChainMap[int, str]] = collections.defaultdict(
|
self._game_store: typing.Dict[str, typing.ChainMap[int, str]] = collections.defaultdict(
|
||||||
lambda: collections.ChainMap(self._archipelago_lookup, Utils.KeyedDefaultDict(self._unknown_item)))
|
lambda: collections.ChainMap(self._archipelago_lookup, Utils.KeyedDefaultDict(self._unknown_item)))
|
||||||
self.warned: bool = False
|
|
||||||
|
|
||||||
# noinspection PyTypeChecker
|
# noinspection PyTypeChecker
|
||||||
def __getitem__(self, key: str) -> typing.Mapping[int, str]:
|
def __getitem__(self, key: str) -> typing.Mapping[int, str]:
|
||||||
# TODO: In a future version (0.6.0?) this should be simplified by removing implicit id lookups support.
|
|
||||||
if isinstance(key, int):
|
|
||||||
if not self.warned:
|
|
||||||
# Use warnings instead of logger to avoid deprecation message from appearing on user side.
|
|
||||||
self.warned = True
|
|
||||||
warnings.warn(f"Implicit name lookup by id only is deprecated and only supported to maintain "
|
|
||||||
f"backwards compatibility for now. If multiple games share the same id for a "
|
|
||||||
f"{self.lookup_type}, name could be incorrect. Please use "
|
|
||||||
f"`{self.lookup_type}_names.lookup_in_game()` or "
|
|
||||||
f"`{self.lookup_type}_names.lookup_in_slot()` instead.")
|
|
||||||
return self._flat_store[key] # type: ignore
|
|
||||||
|
|
||||||
return self._game_store[key]
|
return self._game_store[key]
|
||||||
|
|
||||||
def __len__(self) -> int:
|
def __len__(self) -> int:
|
||||||
@@ -254,7 +240,6 @@ class CommonContext:
|
|||||||
id_to_name_lookup_table = Utils.KeyedDefaultDict(self._unknown_item)
|
id_to_name_lookup_table = Utils.KeyedDefaultDict(self._unknown_item)
|
||||||
id_to_name_lookup_table.update({code: name for name, code in name_to_id_lookup_table.items()})
|
id_to_name_lookup_table.update({code: name for name, code in name_to_id_lookup_table.items()})
|
||||||
self._game_store[game] = collections.ChainMap(self._archipelago_lookup, id_to_name_lookup_table)
|
self._game_store[game] = collections.ChainMap(self._archipelago_lookup, id_to_name_lookup_table)
|
||||||
self._flat_store.update(id_to_name_lookup_table) # Only needed for legacy lookup method.
|
|
||||||
if game == "Archipelago":
|
if game == "Archipelago":
|
||||||
# Keep track of the Archipelago data package separately so if it gets updated in a custom datapackage,
|
# Keep track of the Archipelago data package separately so if it gets updated in a custom datapackage,
|
||||||
# it updates in all chain maps automatically.
|
# it updates in all chain maps automatically.
|
||||||
@@ -356,7 +341,6 @@ class CommonContext:
|
|||||||
|
|
||||||
self.item_names = self.NameLookupDict(self, "item")
|
self.item_names = self.NameLookupDict(self, "item")
|
||||||
self.location_names = self.NameLookupDict(self, "location")
|
self.location_names = self.NameLookupDict(self, "location")
|
||||||
self.versions = {}
|
|
||||||
self.checksums = {}
|
self.checksums = {}
|
||||||
|
|
||||||
self.jsontotextparser = JSONtoTextParser(self)
|
self.jsontotextparser = JSONtoTextParser(self)
|
||||||
@@ -571,7 +555,6 @@ class CommonContext:
|
|||||||
|
|
||||||
# DataPackage
|
# DataPackage
|
||||||
async def prepare_data_package(self, relevant_games: typing.Set[str],
|
async def prepare_data_package(self, relevant_games: typing.Set[str],
|
||||||
remote_date_package_versions: typing.Dict[str, int],
|
|
||||||
remote_data_package_checksums: typing.Dict[str, str]):
|
remote_data_package_checksums: typing.Dict[str, str]):
|
||||||
"""Validate that all data is present for the current multiworld.
|
"""Validate that all data is present for the current multiworld.
|
||||||
Download, assimilate and cache missing data from the server."""
|
Download, assimilate and cache missing data from the server."""
|
||||||
@@ -580,33 +563,26 @@ class CommonContext:
|
|||||||
|
|
||||||
needed_updates: typing.Set[str] = set()
|
needed_updates: typing.Set[str] = set()
|
||||||
for game in relevant_games:
|
for game in relevant_games:
|
||||||
if game not in remote_date_package_versions and game not in remote_data_package_checksums:
|
if game not in remote_data_package_checksums:
|
||||||
continue
|
continue
|
||||||
|
|
||||||
remote_version: int = remote_date_package_versions.get(game, 0)
|
|
||||||
remote_checksum: typing.Optional[str] = remote_data_package_checksums.get(game)
|
remote_checksum: typing.Optional[str] = remote_data_package_checksums.get(game)
|
||||||
|
|
||||||
if remote_version == 0 and not remote_checksum: # custom data package and no checksum for this game
|
if not remote_checksum: # custom data package and no checksum for this game
|
||||||
needed_updates.add(game)
|
needed_updates.add(game)
|
||||||
continue
|
continue
|
||||||
|
|
||||||
cached_version: int = self.versions.get(game, 0)
|
|
||||||
cached_checksum: typing.Optional[str] = self.checksums.get(game)
|
cached_checksum: typing.Optional[str] = self.checksums.get(game)
|
||||||
# no action required if cached version is new enough
|
# no action required if cached version is new enough
|
||||||
if (not remote_checksum and (remote_version > cached_version or remote_version == 0)) \
|
if remote_checksum != cached_checksum:
|
||||||
or remote_checksum != cached_checksum:
|
|
||||||
local_version: int = network_data_package["games"].get(game, {}).get("version", 0)
|
|
||||||
local_checksum: typing.Optional[str] = network_data_package["games"].get(game, {}).get("checksum")
|
local_checksum: typing.Optional[str] = network_data_package["games"].get(game, {}).get("checksum")
|
||||||
if ((remote_checksum or remote_version <= local_version and remote_version != 0)
|
if remote_checksum == local_checksum:
|
||||||
and remote_checksum == local_checksum):
|
|
||||||
self.update_game(network_data_package["games"][game], game)
|
self.update_game(network_data_package["games"][game], game)
|
||||||
else:
|
else:
|
||||||
cached_game = Utils.load_data_package_for_checksum(game, remote_checksum)
|
cached_game = Utils.load_data_package_for_checksum(game, remote_checksum)
|
||||||
cache_version: int = cached_game.get("version", 0)
|
|
||||||
cache_checksum: typing.Optional[str] = cached_game.get("checksum")
|
cache_checksum: typing.Optional[str] = cached_game.get("checksum")
|
||||||
# download remote version if cache is not new enough
|
# download remote version if cache is not new enough
|
||||||
if (not remote_checksum and (remote_version > cache_version or remote_version == 0)) \
|
if remote_checksum != cache_checksum:
|
||||||
or remote_checksum != cache_checksum:
|
|
||||||
needed_updates.add(game)
|
needed_updates.add(game)
|
||||||
else:
|
else:
|
||||||
self.update_game(cached_game, game)
|
self.update_game(cached_game, game)
|
||||||
@@ -616,7 +592,6 @@ class CommonContext:
|
|||||||
def update_game(self, game_package: dict, game: str):
|
def update_game(self, game_package: dict, game: str):
|
||||||
self.item_names.update_game(game, game_package["item_name_to_id"])
|
self.item_names.update_game(game, game_package["item_name_to_id"])
|
||||||
self.location_names.update_game(game, game_package["location_name_to_id"])
|
self.location_names.update_game(game, game_package["location_name_to_id"])
|
||||||
self.versions[game] = game_package.get("version", 0)
|
|
||||||
self.checksums[game] = game_package.get("checksum")
|
self.checksums[game] = game_package.get("checksum")
|
||||||
|
|
||||||
def update_data_package(self, data_package: dict):
|
def update_data_package(self, data_package: dict):
|
||||||
@@ -887,9 +862,8 @@ async def process_server_cmd(ctx: CommonContext, args: dict):
|
|||||||
logger.info(' %s (Player %d)' % (network_player.alias, network_player.slot))
|
logger.info(' %s (Player %d)' % (network_player.alias, network_player.slot))
|
||||||
|
|
||||||
# update data package
|
# update data package
|
||||||
data_package_versions = args.get("datapackage_versions", {})
|
|
||||||
data_package_checksums = args.get("datapackage_checksums", {})
|
data_package_checksums = args.get("datapackage_checksums", {})
|
||||||
await ctx.prepare_data_package(set(args["games"]), data_package_versions, data_package_checksums)
|
await ctx.prepare_data_package(set(args["games"]), data_package_checksums)
|
||||||
|
|
||||||
await ctx.server_auth(args['password'])
|
await ctx.server_auth(args['password'])
|
||||||
|
|
||||||
|
|||||||
@@ -1,12 +0,0 @@
|
|||||||
from __future__ import annotations
|
|
||||||
|
|
||||||
import ModuleUpdate
|
|
||||||
ModuleUpdate.update()
|
|
||||||
|
|
||||||
from worlds.factorio.Client import check_stdin, launch
|
|
||||||
import Utils
|
|
||||||
|
|
||||||
if __name__ == "__main__":
|
|
||||||
Utils.init_logging("FactorioClient", exception_logger="Client")
|
|
||||||
check_stdin()
|
|
||||||
launch()
|
|
||||||
23
Generate.py
23
Generate.py
@@ -252,7 +252,20 @@ def read_weights_yamls(path) -> Tuple[Any, ...]:
|
|||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise Exception(f"Failed to read weights ({path})") from e
|
raise Exception(f"Failed to read weights ({path})") from e
|
||||||
|
|
||||||
return tuple(parse_yamls(yaml))
|
from yaml.error import MarkedYAMLError
|
||||||
|
try:
|
||||||
|
return tuple(parse_yamls(yaml))
|
||||||
|
except MarkedYAMLError as ex:
|
||||||
|
if ex.problem_mark:
|
||||||
|
lines = yaml.splitlines()
|
||||||
|
if ex.context_mark:
|
||||||
|
relevant_lines = "\n".join(lines[ex.context_mark.line:ex.problem_mark.line+1])
|
||||||
|
else:
|
||||||
|
relevant_lines = lines[ex.problem_mark.line]
|
||||||
|
error_line = " " * ex.problem_mark.column + "^"
|
||||||
|
raise Exception(f"{ex.context} {ex.problem} on line {ex.problem_mark.line}:"
|
||||||
|
f"\n{relevant_lines}\n{error_line}")
|
||||||
|
raise ex
|
||||||
|
|
||||||
|
|
||||||
def interpret_on_off(value) -> bool:
|
def interpret_on_off(value) -> bool:
|
||||||
@@ -456,6 +469,14 @@ def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str,
|
|||||||
|
|
||||||
|
|
||||||
def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.bosses):
|
def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.bosses):
|
||||||
|
"""
|
||||||
|
Roll options from specified weights, usually originating from a .yaml options file.
|
||||||
|
|
||||||
|
Important note:
|
||||||
|
The same weights dict is shared between all slots using the same yaml (e.g. generic weights file for filler slots).
|
||||||
|
This means it should never be modified without making a deepcopy first.
|
||||||
|
"""
|
||||||
|
|
||||||
from worlds import AutoWorldRegister
|
from worlds import AutoWorldRegister
|
||||||
|
|
||||||
if "linked_options" in weights:
|
if "linked_options" in weights:
|
||||||
|
|||||||
52
Launcher.py
52
Launcher.py
@@ -1,11 +1,11 @@
|
|||||||
"""
|
"""
|
||||||
Archipelago Launcher
|
Archipelago Launcher
|
||||||
|
|
||||||
* if run with APBP as argument, launch corresponding client.
|
* If run with a patch file as argument, launch corresponding client with the patch file as an argument.
|
||||||
* if run with executable as argument, run it passing argv[2:] as arguments
|
* If run with component name as argument, run it passing argv[2:] as arguments.
|
||||||
* if run without arguments, open launcher GUI
|
* If run without arguments or unknown arguments, open launcher GUI.
|
||||||
|
|
||||||
Scroll down to components= to add components to the launcher as well as setup.py
|
Additional components can be added to worlds.LauncherComponents.components.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
import argparse
|
import argparse
|
||||||
@@ -84,12 +84,16 @@ def browse_files():
|
|||||||
def open_folder(folder_path):
|
def open_folder(folder_path):
|
||||||
if is_linux:
|
if is_linux:
|
||||||
exe = which('xdg-open') or which('gnome-open') or which('kde-open')
|
exe = which('xdg-open') or which('gnome-open') or which('kde-open')
|
||||||
subprocess.Popen([exe, folder_path])
|
|
||||||
elif is_macos:
|
elif is_macos:
|
||||||
exe = which("open")
|
exe = which("open")
|
||||||
subprocess.Popen([exe, folder_path])
|
|
||||||
else:
|
else:
|
||||||
webbrowser.open(folder_path)
|
webbrowser.open(folder_path)
|
||||||
|
return
|
||||||
|
|
||||||
|
if exe:
|
||||||
|
subprocess.Popen([exe, folder_path])
|
||||||
|
else:
|
||||||
|
logging.warning(f"No file browser available to open {folder_path}")
|
||||||
|
|
||||||
|
|
||||||
def update_settings():
|
def update_settings():
|
||||||
@@ -230,10 +234,11 @@ def run_gui(path: str, args: Any) -> None:
|
|||||||
from kivy.properties import ObjectProperty
|
from kivy.properties import ObjectProperty
|
||||||
from kivy.core.window import Window
|
from kivy.core.window import Window
|
||||||
from kivy.metrics import dp
|
from kivy.metrics import dp
|
||||||
from kivymd.uix.button import MDIconButton
|
from kivymd.uix.button import MDIconButton, MDButton
|
||||||
from kivymd.uix.card import MDCard
|
from kivymd.uix.card import MDCard
|
||||||
from kivymd.uix.menu import MDDropdownMenu
|
from kivymd.uix.menu import MDDropdownMenu
|
||||||
from kivymd.uix.snackbar import MDSnackbar, MDSnackbarText
|
from kivymd.uix.snackbar import MDSnackbar, MDSnackbarText
|
||||||
|
from kivymd.uix.textfield import MDTextField
|
||||||
|
|
||||||
from kivy.lang.builder import Builder
|
from kivy.lang.builder import Builder
|
||||||
|
|
||||||
@@ -253,6 +258,7 @@ def run_gui(path: str, args: Any) -> None:
|
|||||||
navigation: MDGridLayout = ObjectProperty(None)
|
navigation: MDGridLayout = ObjectProperty(None)
|
||||||
grid: MDGridLayout = ObjectProperty(None)
|
grid: MDGridLayout = ObjectProperty(None)
|
||||||
button_layout: ScrollBox = ObjectProperty(None)
|
button_layout: ScrollBox = ObjectProperty(None)
|
||||||
|
search_box: MDTextField = ObjectProperty(None)
|
||||||
cards: list[LauncherCard]
|
cards: list[LauncherCard]
|
||||||
current_filter: Sequence[str | Type] | None
|
current_filter: Sequence[str | Type] | None
|
||||||
|
|
||||||
@@ -338,14 +344,29 @@ def run_gui(path: str, args: Any) -> None:
|
|||||||
scroll_percent = self.button_layout.convert_distance_to_scroll(0, top)
|
scroll_percent = self.button_layout.convert_distance_to_scroll(0, top)
|
||||||
self.button_layout.scroll_y = max(0, min(1, scroll_percent[1]))
|
self.button_layout.scroll_y = max(0, min(1, scroll_percent[1]))
|
||||||
|
|
||||||
def filter_clients(self, caller):
|
def filter_clients_by_type(self, caller: MDButton):
|
||||||
self._refresh_components(caller.type)
|
self._refresh_components(caller.type)
|
||||||
|
self.search_box.text = ""
|
||||||
|
|
||||||
|
def filter_clients_by_name(self, caller: MDTextField, name: str) -> None:
|
||||||
|
if len(name) == 0:
|
||||||
|
self._refresh_components(self.current_filter)
|
||||||
|
return
|
||||||
|
|
||||||
|
sub_matches = [
|
||||||
|
card for card in self.cards
|
||||||
|
if name.lower() in card.component.display_name.lower() and card.component.type != Type.HIDDEN
|
||||||
|
]
|
||||||
|
self.button_layout.layout.clear_widgets()
|
||||||
|
for card in sub_matches:
|
||||||
|
self.button_layout.layout.add_widget(card)
|
||||||
|
|
||||||
def build(self):
|
def build(self):
|
||||||
self.top_screen = Builder.load_file(Utils.local_path("data/launcher.kv"))
|
self.top_screen = Builder.load_file(Utils.local_path("data/launcher.kv"))
|
||||||
self.grid = self.top_screen.ids.grid
|
self.grid = self.top_screen.ids.grid
|
||||||
self.navigation = self.top_screen.ids.navigation
|
self.navigation = self.top_screen.ids.navigation
|
||||||
self.button_layout = self.top_screen.ids.button_layout
|
self.button_layout = self.top_screen.ids.button_layout
|
||||||
|
self.search_box = self.top_screen.ids.search_box
|
||||||
self.set_colors()
|
self.set_colors()
|
||||||
self.top_screen.md_bg_color = self.theme_cls.backgroundColor
|
self.top_screen.md_bg_color = self.theme_cls.backgroundColor
|
||||||
|
|
||||||
@@ -353,12 +374,18 @@ def run_gui(path: str, args: Any) -> None:
|
|||||||
refresh_components = self._refresh_components
|
refresh_components = self._refresh_components
|
||||||
|
|
||||||
Window.bind(on_drop_file=self._on_drop_file)
|
Window.bind(on_drop_file=self._on_drop_file)
|
||||||
|
Window.bind(on_keyboard=self._on_keyboard)
|
||||||
|
|
||||||
for component in components:
|
for component in components:
|
||||||
self.cards.append(self.build_card(component))
|
self.cards.append(self.build_card(component))
|
||||||
|
|
||||||
self._refresh_components(self.current_filter)
|
self._refresh_components(self.current_filter)
|
||||||
|
|
||||||
|
# Uncomment to re-enable the Kivy console/live editor
|
||||||
|
# Ctrl-E to enable it, make sure numlock/capslock is disabled
|
||||||
|
# from kivy.modules.console import create_console
|
||||||
|
# create_console(Window, self.top_screen)
|
||||||
|
|
||||||
return self.top_screen
|
return self.top_screen
|
||||||
|
|
||||||
def on_start(self):
|
def on_start(self):
|
||||||
@@ -384,6 +411,15 @@ def run_gui(path: str, args: Any) -> None:
|
|||||||
else:
|
else:
|
||||||
logging.warning(f"unable to identify component for {file}")
|
logging.warning(f"unable to identify component for {file}")
|
||||||
|
|
||||||
|
def _on_keyboard(self, window: Window, key: int, scancode: int, codepoint: str, modifier: list[str]):
|
||||||
|
# Activate search as soon as we start typing, no matter if we are focused on the search box or not.
|
||||||
|
# Focus first, then capture the first character we type, otherwise it gets swallowed and lost.
|
||||||
|
# Limit text input to ASCII non-control characters (space bar to tilde).
|
||||||
|
if not self.search_box.focus:
|
||||||
|
self.search_box.focus = True
|
||||||
|
if key in range(32, 126):
|
||||||
|
self.search_box.text += codepoint
|
||||||
|
|
||||||
def _stop(self, *largs):
|
def _stop(self, *largs):
|
||||||
# ran into what appears to be https://groups.google.com/g/kivy-users/c/saWDLoYCSZ4 with PyCharm.
|
# ran into what appears to be https://groups.google.com/g/kivy-users/c/saWDLoYCSZ4 with PyCharm.
|
||||||
# Closing the window explicitly cleans it up.
|
# Closing the window explicitly cleans it up.
|
||||||
|
|||||||
@@ -33,7 +33,7 @@ from worlds.ladx.TrackerConsts import storage_key
|
|||||||
from worlds.ladx.ItemTracker import ItemTracker
|
from worlds.ladx.ItemTracker import ItemTracker
|
||||||
from worlds.ladx.LADXR.checkMetadata import checkMetadataTable
|
from worlds.ladx.LADXR.checkMetadata import checkMetadataTable
|
||||||
from worlds.ladx.Locations import get_locations_to_id, meta_to_name
|
from worlds.ladx.Locations import get_locations_to_id, meta_to_name
|
||||||
from worlds.ladx.Tracker import LocationTracker, MagpieBridge
|
from worlds.ladx.Tracker import LocationTracker, MagpieBridge, Check
|
||||||
|
|
||||||
|
|
||||||
class GameboyException(Exception):
|
class GameboyException(Exception):
|
||||||
@@ -52,22 +52,6 @@ class BadRetroArchResponse(GameboyException):
|
|||||||
pass
|
pass
|
||||||
|
|
||||||
|
|
||||||
def magpie_logo():
|
|
||||||
from kivy.uix.image import CoreImage
|
|
||||||
binary_data = """
|
|
||||||
iVBORw0KGgoAAAANSUhEUgAAABAAAAAQCAIAAACQkWg2AAAAAXN
|
|
||||||
SR0IArs4c6QAAAARnQU1BAACxjwv8YQUAAAAJcEhZcwAADsMAAA
|
|
||||||
7DAcdvqGQAAADGSURBVDhPhVLBEcIwDHOYhjHCBuXHj2OTbAL8+
|
|
||||||
MEGZIxOQ1CinOOk0Op0bmo7tlXXeR9FJMYDLOD9mwcLjQK7+hSZ
|
|
||||||
wgcWMZJOAGeGKtChNHFL0j+FZD3jSCuo0w7l03wDrWdg00C4/aW
|
|
||||||
eDEYNenuzPOfPspBnxf0kssE80vN0L8361j10P03DK4x6FHabuV
|
|
||||||
ear8fHme+b17rwSjbAXeUMLb+EVTV2QHm46MWQanmnydA98KsVS
|
|
||||||
XkV+qFpGQXrLhT/fqraQeQLuplpNH5g+WkAAAAASUVORK5CYII="""
|
|
||||||
binary_data = base64.b64decode(binary_data)
|
|
||||||
data = io.BytesIO(binary_data)
|
|
||||||
return CoreImage(data, ext="png").texture
|
|
||||||
|
|
||||||
|
|
||||||
class LAClientConstants:
|
class LAClientConstants:
|
||||||
# Connector version
|
# Connector version
|
||||||
VERSION = 0x01
|
VERSION = 0x01
|
||||||
@@ -530,7 +514,9 @@ class LinksAwakeningContext(CommonContext):
|
|||||||
|
|
||||||
def run_gui(self) -> None:
|
def run_gui(self) -> None:
|
||||||
import webbrowser
|
import webbrowser
|
||||||
from kvui import GameManager, ImageButton
|
from kvui import GameManager
|
||||||
|
from kivy.metrics import dp
|
||||||
|
from kivymd.uix.button import MDButton, MDButtonText
|
||||||
|
|
||||||
class LADXManager(GameManager):
|
class LADXManager(GameManager):
|
||||||
logging_pairs = [
|
logging_pairs = [
|
||||||
@@ -543,8 +529,10 @@ class LinksAwakeningContext(CommonContext):
|
|||||||
b = super().build()
|
b = super().build()
|
||||||
|
|
||||||
if self.ctx.magpie_enabled:
|
if self.ctx.magpie_enabled:
|
||||||
button = ImageButton(texture=magpie_logo(), fit_mode="cover", image_size=(32, 32), size_hint_x=None,
|
button = MDButton(MDButtonText(text="Open Tracker"), style="filled", size=(dp(100), dp(70)), radius=5,
|
||||||
on_press=lambda _: webbrowser.open('https://magpietracker.us/?enable_autotracker=1'))
|
size_hint_x=None, size_hint_y=None, pos_hint={"center_y": 0.55},
|
||||||
|
on_press=lambda _: webbrowser.open('https://magpietracker.us/?enable_autotracker=1'))
|
||||||
|
button.height = self.server_connect_bar.height
|
||||||
self.connect_layout.add_widget(button)
|
self.connect_layout.add_widget(button)
|
||||||
|
|
||||||
return b
|
return b
|
||||||
@@ -638,6 +626,11 @@ class LinksAwakeningContext(CommonContext):
|
|||||||
"password": self.password,
|
"password": self.password,
|
||||||
})
|
})
|
||||||
|
|
||||||
|
# We can process linked items on already-checked checks now that we have slot_data
|
||||||
|
if self.client.tracker:
|
||||||
|
checked_checks = set(self.client.tracker.all_checks) - set(self.client.tracker.remaining_checks)
|
||||||
|
self.add_linked_items(checked_checks)
|
||||||
|
|
||||||
# TODO - use watcher_event
|
# TODO - use watcher_event
|
||||||
if cmd == "ReceivedItems":
|
if cmd == "ReceivedItems":
|
||||||
for index, item in enumerate(args["items"], start=args["index"]):
|
for index, item in enumerate(args["items"], start=args["index"]):
|
||||||
@@ -653,6 +646,13 @@ class LinksAwakeningContext(CommonContext):
|
|||||||
sync_msg = [{'cmd': 'Sync'}]
|
sync_msg = [{'cmd': 'Sync'}]
|
||||||
await self.send_msgs(sync_msg)
|
await self.send_msgs(sync_msg)
|
||||||
|
|
||||||
|
def add_linked_items(self, checks: typing.List[Check]):
|
||||||
|
for check in checks:
|
||||||
|
if check.value and check.linkedItem:
|
||||||
|
linkedItem = check.linkedItem
|
||||||
|
if 'condition' not in linkedItem or (self.slot_data and linkedItem['condition'](self.slot_data)):
|
||||||
|
self.client.item_tracker.setExtraItem(check.linkedItem['item'], check.linkedItem['qty'])
|
||||||
|
|
||||||
item_id_lookup = get_locations_to_id()
|
item_id_lookup = get_locations_to_id()
|
||||||
|
|
||||||
async def run_game_loop(self):
|
async def run_game_loop(self):
|
||||||
@@ -661,11 +661,7 @@ class LinksAwakeningContext(CommonContext):
|
|||||||
checkMetadataTable[check.id])] for check in ladxr_checks]
|
checkMetadataTable[check.id])] for check in ladxr_checks]
|
||||||
self.new_checks(checks, [check.id for check in ladxr_checks])
|
self.new_checks(checks, [check.id for check in ladxr_checks])
|
||||||
|
|
||||||
for check in ladxr_checks:
|
self.add_linked_items(ladxr_checks)
|
||||||
if check.value and check.linkedItem:
|
|
||||||
linkedItem = check.linkedItem
|
|
||||||
if 'condition' not in linkedItem or linkedItem['condition'](self.slot_data):
|
|
||||||
self.client.item_tracker.setExtraItem(check.linkedItem['item'], check.linkedItem['qty'])
|
|
||||||
|
|
||||||
async def victory():
|
async def victory():
|
||||||
await self.send_victory()
|
await self.send_victory()
|
||||||
|
|||||||
21
Main.py
21
Main.py
@@ -56,29 +56,15 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
logger.info(f"Found {len(AutoWorld.AutoWorldRegister.world_types)} World Types:")
|
logger.info(f"Found {len(AutoWorld.AutoWorldRegister.world_types)} World Types:")
|
||||||
longest_name = max(len(text) for text in AutoWorld.AutoWorldRegister.world_types)
|
longest_name = max(len(text) for text in AutoWorld.AutoWorldRegister.world_types)
|
||||||
|
|
||||||
max_item = 0
|
|
||||||
max_location = 0
|
|
||||||
for cls in AutoWorld.AutoWorldRegister.world_types.values():
|
|
||||||
if cls.item_id_to_name:
|
|
||||||
max_item = max(max_item, max(cls.item_id_to_name))
|
|
||||||
max_location = max(max_location, max(cls.location_id_to_name))
|
|
||||||
|
|
||||||
item_digits = len(str(max_item))
|
|
||||||
location_digits = len(str(max_location))
|
|
||||||
item_count = len(str(max(len(cls.item_names) for cls in AutoWorld.AutoWorldRegister.world_types.values())))
|
item_count = len(str(max(len(cls.item_names) for cls in AutoWorld.AutoWorldRegister.world_types.values())))
|
||||||
location_count = len(str(max(len(cls.location_names) for cls in AutoWorld.AutoWorldRegister.world_types.values())))
|
location_count = len(str(max(len(cls.location_names) for cls in AutoWorld.AutoWorldRegister.world_types.values())))
|
||||||
del max_item, max_location
|
|
||||||
|
|
||||||
for name, cls in AutoWorld.AutoWorldRegister.world_types.items():
|
for name, cls in AutoWorld.AutoWorldRegister.world_types.items():
|
||||||
if not cls.hidden and len(cls.item_names) > 0:
|
if not cls.hidden and len(cls.item_names) > 0:
|
||||||
logger.info(f" {name:{longest_name}}: {len(cls.item_names):{item_count}} "
|
logger.info(f" {name:{longest_name}}: Items: {len(cls.item_names):{item_count}} | "
|
||||||
f"Items (IDs: {min(cls.item_id_to_name):{item_digits}} - "
|
f"Locations: {len(cls.location_names):{location_count}}")
|
||||||
f"{max(cls.item_id_to_name):{item_digits}}) | "
|
|
||||||
f"{len(cls.location_names):{location_count}} "
|
|
||||||
f"Locations (IDs: {min(cls.location_id_to_name):{location_digits}} - "
|
|
||||||
f"{max(cls.location_id_to_name):{location_digits}})")
|
|
||||||
|
|
||||||
del item_digits, location_digits, item_count, location_count
|
del item_count, location_count
|
||||||
|
|
||||||
# This assertion method should not be necessary to run if we are not outputting any multidata.
|
# This assertion method should not be necessary to run if we are not outputting any multidata.
|
||||||
if not args.skip_output and not args.spoiler_only:
|
if not args.skip_output and not args.spoiler_only:
|
||||||
@@ -315,6 +301,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
|||||||
game_world.game: worlds.network_data_package["games"][game_world.game]
|
game_world.game: worlds.network_data_package["games"][game_world.game]
|
||||||
for game_world in multiworld.worlds.values()
|
for game_world in multiworld.worlds.values()
|
||||||
}
|
}
|
||||||
|
data_package["Archipelago"] = worlds.network_data_package["games"]["Archipelago"]
|
||||||
|
|
||||||
checks_in_area: Dict[int, Dict[str, Union[int, List[int]]]] = {}
|
checks_in_area: Dict[int, Dict[str, Union[int, List[int]]]] = {}
|
||||||
|
|
||||||
|
|||||||
@@ -46,7 +46,8 @@ from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, Networ
|
|||||||
SlotType, LocationStore, Hint, HintStatus
|
SlotType, LocationStore, Hint, HintStatus
|
||||||
from BaseClasses import ItemClassification
|
from BaseClasses import ItemClassification
|
||||||
|
|
||||||
min_client_version = Version(0, 1, 6)
|
|
||||||
|
min_client_version = Version(0, 5, 0)
|
||||||
colorama.just_fix_windows_console()
|
colorama.just_fix_windows_console()
|
||||||
|
|
||||||
|
|
||||||
@@ -1982,11 +1983,13 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
|||||||
new_hint = new_hint.re_prioritize(ctx, status)
|
new_hint = new_hint.re_prioritize(ctx, status)
|
||||||
if hint == new_hint:
|
if hint == new_hint:
|
||||||
return
|
return
|
||||||
ctx.replace_hint(client.team, hint.finding_player, hint, new_hint)
|
|
||||||
ctx.replace_hint(client.team, hint.receiving_player, hint, new_hint)
|
concerning_slots = ctx.slot_set(hint.receiving_player) | {hint.finding_player}
|
||||||
|
for slot in concerning_slots:
|
||||||
|
ctx.replace_hint(client.team, slot, hint, new_hint)
|
||||||
ctx.save()
|
ctx.save()
|
||||||
ctx.on_changed_hints(client.team, hint.finding_player)
|
for slot in concerning_slots:
|
||||||
ctx.on_changed_hints(client.team, hint.receiving_player)
|
ctx.on_changed_hints(client.team, slot)
|
||||||
|
|
||||||
elif cmd == 'StatusUpdate':
|
elif cmd == 'StatusUpdate':
|
||||||
update_client_status(ctx, client, args["status"])
|
update_client_status(ctx, client, args["status"])
|
||||||
@@ -2416,8 +2419,10 @@ async def console(ctx: Context):
|
|||||||
|
|
||||||
|
|
||||||
def parse_args() -> argparse.Namespace:
|
def parse_args() -> argparse.Namespace:
|
||||||
|
from settings import get_settings
|
||||||
|
|
||||||
parser = argparse.ArgumentParser()
|
parser = argparse.ArgumentParser()
|
||||||
defaults = Utils.get_settings()["server_options"].as_dict()
|
defaults = get_settings().server_options.as_dict()
|
||||||
parser.add_argument('multidata', nargs="?", default=defaults["multidata"])
|
parser.add_argument('multidata', nargs="?", default=defaults["multidata"])
|
||||||
parser.add_argument('--host', default=defaults["host"])
|
parser.add_argument('--host', default=defaults["host"])
|
||||||
parser.add_argument('--port', default=defaults["port"], type=int)
|
parser.add_argument('--port', default=defaults["port"], type=int)
|
||||||
|
|||||||
109
Options.py
109
Options.py
@@ -1,6 +1,7 @@
|
|||||||
from __future__ import annotations
|
from __future__ import annotations
|
||||||
|
|
||||||
import abc
|
import abc
|
||||||
|
import collections
|
||||||
import functools
|
import functools
|
||||||
import logging
|
import logging
|
||||||
import math
|
import math
|
||||||
@@ -866,15 +867,49 @@ class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys, typing.Mappin
|
|||||||
def __len__(self) -> int:
|
def __len__(self) -> int:
|
||||||
return self.value.__len__()
|
return self.value.__len__()
|
||||||
|
|
||||||
|
# __getitem__ fallback fails for Counters, so we define this explicitly
|
||||||
|
def __contains__(self, item) -> bool:
|
||||||
|
return item in self.value
|
||||||
|
|
||||||
class ItemDict(OptionDict):
|
|
||||||
|
class OptionCounter(OptionDict):
|
||||||
|
min: int | None = None
|
||||||
|
max: int | None = None
|
||||||
|
|
||||||
|
def __init__(self, value: dict[str, int]) -> None:
|
||||||
|
super(OptionCounter, self).__init__(collections.Counter(value))
|
||||||
|
|
||||||
|
def verify(self, world: type[World], player_name: str, plando_options: PlandoOptions) -> None:
|
||||||
|
super(OptionCounter, self).verify(world, player_name, plando_options)
|
||||||
|
|
||||||
|
range_errors = []
|
||||||
|
|
||||||
|
if self.max is not None:
|
||||||
|
range_errors += [
|
||||||
|
f"\"{key}: {value}\" is higher than maximum allowed value {self.max}."
|
||||||
|
for key, value in self.value.items() if value > self.max
|
||||||
|
]
|
||||||
|
|
||||||
|
if self.min is not None:
|
||||||
|
range_errors += [
|
||||||
|
f"\"{key}: {value}\" is lower than minimum allowed value {self.min}."
|
||||||
|
for key, value in self.value.items() if value < self.min
|
||||||
|
]
|
||||||
|
|
||||||
|
if range_errors:
|
||||||
|
range_errors = [f"For option {getattr(self, 'display_name', self)}:"] + range_errors
|
||||||
|
raise OptionError("\n".join(range_errors))
|
||||||
|
|
||||||
|
|
||||||
|
class ItemDict(OptionCounter):
|
||||||
verify_item_name = True
|
verify_item_name = True
|
||||||
|
|
||||||
def __init__(self, value: typing.Dict[str, int]):
|
min = 0
|
||||||
if any(item_count is None for item_count in value.values()):
|
|
||||||
raise Exception("Items must have counts associated with them. Please provide positive integer values in the format \"item\": count .")
|
def __init__(self, value: dict[str, int]) -> None:
|
||||||
if any(item_count < 1 for item_count in value.values()):
|
# Backwards compatibility: Cull 0s to make "in" checks behave the same as when this wasn't a OptionCounter
|
||||||
raise Exception("Cannot have non-positive item counts.")
|
value = {item_name: amount for item_name, amount in value.items() if amount != 0}
|
||||||
|
|
||||||
super(ItemDict, self).__init__(value)
|
super(ItemDict, self).__init__(value)
|
||||||
|
|
||||||
|
|
||||||
@@ -1257,42 +1292,47 @@ class CommonOptions(metaclass=OptionsMetaProperty):
|
|||||||
progression_balancing: ProgressionBalancing
|
progression_balancing: ProgressionBalancing
|
||||||
accessibility: Accessibility
|
accessibility: Accessibility
|
||||||
|
|
||||||
def as_dict(self,
|
def as_dict(
|
||||||
*option_names: str,
|
self,
|
||||||
casing: typing.Literal["snake", "camel", "pascal", "kebab"] = "snake",
|
*option_names: str,
|
||||||
toggles_as_bools: bool = False) -> typing.Dict[str, typing.Any]:
|
casing: typing.Literal["snake", "camel", "pascal", "kebab"] = "snake",
|
||||||
|
toggles_as_bools: bool = False,
|
||||||
|
) -> dict[str, typing.Any]:
|
||||||
"""
|
"""
|
||||||
Returns a dictionary of [str, Option.value]
|
Returns a dictionary of [str, Option.value]
|
||||||
|
|
||||||
:param option_names: names of the options to return
|
:param option_names: Names of the options to get the values of.
|
||||||
:param casing: case of the keys to return. Supports `snake`, `camel`, `pascal`, `kebab`
|
:param casing: Casing of the keys to return. Supports `snake`, `camel`, `pascal`, `kebab`.
|
||||||
:param toggles_as_bools: whether toggle options should be output as bools instead of strings
|
:param toggles_as_bools: Whether toggle options should be returned as bools instead of ints.
|
||||||
|
|
||||||
|
:return: A dictionary of each option name to the value of its Option. If the option is an OptionSet, the value
|
||||||
|
will be returned as a sorted list.
|
||||||
"""
|
"""
|
||||||
assert option_names, "options.as_dict() was used without any option names."
|
assert option_names, "options.as_dict() was used without any option names."
|
||||||
option_results = {}
|
option_results = {}
|
||||||
for option_name in option_names:
|
for option_name in option_names:
|
||||||
if option_name in type(self).type_hints:
|
if option_name not in type(self).type_hints:
|
||||||
if casing == "snake":
|
|
||||||
display_name = option_name
|
|
||||||
elif casing == "camel":
|
|
||||||
split_name = [name.title() for name in option_name.split("_")]
|
|
||||||
split_name[0] = split_name[0].lower()
|
|
||||||
display_name = "".join(split_name)
|
|
||||||
elif casing == "pascal":
|
|
||||||
display_name = "".join([name.title() for name in option_name.split("_")])
|
|
||||||
elif casing == "kebab":
|
|
||||||
display_name = option_name.replace("_", "-")
|
|
||||||
else:
|
|
||||||
raise ValueError(f"{casing} is invalid casing for as_dict. "
|
|
||||||
"Valid names are 'snake', 'camel', 'pascal', 'kebab'.")
|
|
||||||
value = getattr(self, option_name).value
|
|
||||||
if isinstance(value, set):
|
|
||||||
value = sorted(value)
|
|
||||||
elif toggles_as_bools and issubclass(type(self).type_hints[option_name], Toggle):
|
|
||||||
value = bool(value)
|
|
||||||
option_results[display_name] = value
|
|
||||||
else:
|
|
||||||
raise ValueError(f"{option_name} not found in {tuple(type(self).type_hints)}")
|
raise ValueError(f"{option_name} not found in {tuple(type(self).type_hints)}")
|
||||||
|
|
||||||
|
if casing == "snake":
|
||||||
|
display_name = option_name
|
||||||
|
elif casing == "camel":
|
||||||
|
split_name = [name.title() for name in option_name.split("_")]
|
||||||
|
split_name[0] = split_name[0].lower()
|
||||||
|
display_name = "".join(split_name)
|
||||||
|
elif casing == "pascal":
|
||||||
|
display_name = "".join([name.title() for name in option_name.split("_")])
|
||||||
|
elif casing == "kebab":
|
||||||
|
display_name = option_name.replace("_", "-")
|
||||||
|
else:
|
||||||
|
raise ValueError(f"{casing} is invalid casing for as_dict. "
|
||||||
|
"Valid names are 'snake', 'camel', 'pascal', 'kebab'.")
|
||||||
|
value = getattr(self, option_name).value
|
||||||
|
if isinstance(value, set):
|
||||||
|
value = sorted(value)
|
||||||
|
elif toggles_as_bools and issubclass(type(self).type_hints[option_name], Toggle):
|
||||||
|
value = bool(value)
|
||||||
|
option_results[display_name] = value
|
||||||
return option_results
|
return option_results
|
||||||
|
|
||||||
|
|
||||||
@@ -1313,6 +1353,7 @@ class StartInventory(ItemDict):
|
|||||||
verify_item_name = True
|
verify_item_name = True
|
||||||
display_name = "Start Inventory"
|
display_name = "Start Inventory"
|
||||||
rich_text_doc = True
|
rich_text_doc = True
|
||||||
|
max = 10000
|
||||||
|
|
||||||
|
|
||||||
class StartInventoryPool(StartInventory):
|
class StartInventoryPool(StartInventory):
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ Currently, the following games are supported:
|
|||||||
* Factorio
|
* Factorio
|
||||||
* Minecraft
|
* Minecraft
|
||||||
* Subnautica
|
* Subnautica
|
||||||
* Slay the Spire
|
|
||||||
* Risk of Rain 2
|
* Risk of Rain 2
|
||||||
* The Legend of Zelda: Ocarina of Time
|
* The Legend of Zelda: Ocarina of Time
|
||||||
* Timespinner
|
* Timespinner
|
||||||
@@ -63,7 +62,6 @@ Currently, the following games are supported:
|
|||||||
* TUNIC
|
* TUNIC
|
||||||
* Kirby's Dream Land 3
|
* Kirby's Dream Land 3
|
||||||
* Celeste 64
|
* Celeste 64
|
||||||
* Zork Grand Inquisitor
|
|
||||||
* Castlevania 64
|
* Castlevania 64
|
||||||
* A Short Hike
|
* A Short Hike
|
||||||
* Yoshi's Island
|
* Yoshi's Island
|
||||||
|
|||||||
11
Utils.py
11
Utils.py
@@ -114,6 +114,8 @@ def cache_self1(function: typing.Callable[[S, T], RetType]) -> typing.Callable[[
|
|||||||
cache[arg] = res
|
cache[arg] = res
|
||||||
return res
|
return res
|
||||||
|
|
||||||
|
wrap.__defaults__ = function.__defaults__
|
||||||
|
|
||||||
return wrap
|
return wrap
|
||||||
|
|
||||||
|
|
||||||
@@ -427,6 +429,9 @@ class RestrictedUnpickler(pickle.Unpickler):
|
|||||||
def find_class(self, module: str, name: str) -> type:
|
def find_class(self, module: str, name: str) -> type:
|
||||||
if module == "builtins" and name in safe_builtins:
|
if module == "builtins" and name in safe_builtins:
|
||||||
return getattr(builtins, name)
|
return getattr(builtins, name)
|
||||||
|
# used by OptionCounter
|
||||||
|
if module == "collections" and name == "Counter":
|
||||||
|
return collections.Counter
|
||||||
# used by MultiServer -> savegame/multidata
|
# used by MultiServer -> savegame/multidata
|
||||||
if module == "NetUtils" and name in {"NetworkItem", "ClientStatus", "Hint",
|
if module == "NetUtils" and name in {"NetworkItem", "ClientStatus", "Hint",
|
||||||
"SlotType", "NetworkSlot", "HintStatus"}:
|
"SlotType", "NetworkSlot", "HintStatus"}:
|
||||||
@@ -630,6 +635,8 @@ def get_fuzzy_results(input_word: str, word_list: typing.Collection[str], limit:
|
|||||||
import jellyfish
|
import jellyfish
|
||||||
|
|
||||||
def get_fuzzy_ratio(word1: str, word2: str) -> float:
|
def get_fuzzy_ratio(word1: str, word2: str) -> float:
|
||||||
|
if word1 == word2:
|
||||||
|
return 1.01
|
||||||
return (1 - jellyfish.damerau_levenshtein_distance(word1.lower(), word2.lower())
|
return (1 - jellyfish.damerau_levenshtein_distance(word1.lower(), word2.lower())
|
||||||
/ max(len(word1), len(word2)))
|
/ max(len(word1), len(word2)))
|
||||||
|
|
||||||
@@ -650,8 +657,10 @@ def get_intended_text(input_text: str, possible_answers) -> typing.Tuple[str, bo
|
|||||||
picks = get_fuzzy_results(input_text, possible_answers, limit=2)
|
picks = get_fuzzy_results(input_text, possible_answers, limit=2)
|
||||||
if len(picks) > 1:
|
if len(picks) > 1:
|
||||||
dif = picks[0][1] - picks[1][1]
|
dif = picks[0][1] - picks[1][1]
|
||||||
if picks[0][1] == 100:
|
if picks[0][1] == 101:
|
||||||
return picks[0][0], True, "Perfect Match"
|
return picks[0][0], True, "Perfect Match"
|
||||||
|
elif picks[0][1] == 100:
|
||||||
|
return picks[0][0], True, "Case Insensitive Perfect Match"
|
||||||
elif picks[0][1] < 75:
|
elif picks[0][1] < 75:
|
||||||
return picks[0][0], False, f"Didn't find something that closely matches '{input_text}', " \
|
return picks[0][0], False, f"Didn't find something that closely matches '{input_text}', " \
|
||||||
f"did you mean '{picks[0][0]}'? ({picks[0][1]}% sure)"
|
f"did you mean '{picks[0][0]}'? ({picks[0][1]}% sure)"
|
||||||
|
|||||||
@@ -28,6 +28,6 @@ def get_seeds():
|
|||||||
response.append({
|
response.append({
|
||||||
"seed_id": seed.id,
|
"seed_id": seed.id,
|
||||||
"creation_time": seed.creation_time,
|
"creation_time": seed.creation_time,
|
||||||
"players": get_players(seed.slots),
|
"players": get_players(seed),
|
||||||
})
|
})
|
||||||
return jsonify(response)
|
return jsonify(response)
|
||||||
|
|||||||
@@ -108,7 +108,7 @@ def option_presets(game: str) -> Response:
|
|||||||
f"Expected {option.special_range_names.keys()} or {option.range_start}-{option.range_end}."
|
f"Expected {option.special_range_names.keys()} or {option.range_start}-{option.range_end}."
|
||||||
|
|
||||||
presets[preset_name][preset_option_name] = option.value
|
presets[preset_name][preset_option_name] = option.value
|
||||||
elif isinstance(option, (Options.Range, Options.OptionSet, Options.OptionList, Options.ItemDict)):
|
elif isinstance(option, (Options.Range, Options.OptionSet, Options.OptionList, Options.OptionCounter)):
|
||||||
presets[preset_name][preset_option_name] = option.value
|
presets[preset_name][preset_option_name] = option.value
|
||||||
elif isinstance(preset_option, str):
|
elif isinstance(preset_option, str):
|
||||||
# Ensure the option value is valid for Choice and Toggle options
|
# Ensure the option value is valid for Choice and Toggle options
|
||||||
@@ -222,7 +222,7 @@ def generate_yaml(game: str):
|
|||||||
|
|
||||||
for key, val in options.copy().items():
|
for key, val in options.copy().items():
|
||||||
key_parts = key.rsplit("||", 2)
|
key_parts = key.rsplit("||", 2)
|
||||||
# Detect and build ItemDict options from their name pattern
|
# Detect and build OptionCounter options from their name pattern
|
||||||
if key_parts[-1] == "qty":
|
if key_parts[-1] == "qty":
|
||||||
if key_parts[0] not in options:
|
if key_parts[0] not in options:
|
||||||
options[key_parts[0]] = {}
|
options[key_parts[0]] = {}
|
||||||
|
|||||||
@@ -111,10 +111,19 @@
|
|||||||
</div>
|
</div>
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro ItemDict(option_name, option) %}
|
{% macro OptionCounter(option_name, option) %}
|
||||||
|
{% set relevant_keys = option.valid_keys %}
|
||||||
|
{% if not relevant_keys %}
|
||||||
|
{% if option.verify_item_name %}
|
||||||
|
{% set relevant_keys = world.item_names %}
|
||||||
|
{% elif option.verify_location_name %}
|
||||||
|
{% set relevant_keys = world.location_names %}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
{{ OptionTitle(option_name, option) }}
|
{{ OptionTitle(option_name, option) }}
|
||||||
<div class="option-container">
|
<div class="option-container">
|
||||||
{% for item_name in (option.valid_keys|sort if (option.valid_keys|length > 0) else world.item_names|sort) %}
|
{% for item_name in (relevant_keys if relevant_keys is ordered else relevant_keys|sort) %}
|
||||||
<div class="option-entry">
|
<div class="option-entry">
|
||||||
<label for="{{ option_name }}-{{ item_name }}-qty">{{ item_name }}</label>
|
<label for="{{ option_name }}-{{ item_name }}-qty">{{ item_name }}</label>
|
||||||
<input type="number" id="{{ option_name }}-{{ item_name }}-qty" name="{{ option_name }}||{{ item_name }}||qty" value="{{ option.default[item_name]|default("0") }}" data-option-name="{{ option_name }}" data-item-name="{{ item_name }}" />
|
<input type="number" id="{{ option_name }}-{{ item_name }}-qty" name="{{ option_name }}||{{ item_name }}||qty" value="{{ option.default[item_name]|default("0") }}" data-option-name="{{ option_name }}" data-item-name="{{ item_name }}" />
|
||||||
|
|||||||
@@ -93,8 +93,10 @@
|
|||||||
{% elif issubclass(option, Options.FreeText) %}
|
{% elif issubclass(option, Options.FreeText) %}
|
||||||
{{ inputs.FreeText(option_name, option) }}
|
{{ inputs.FreeText(option_name, option) }}
|
||||||
|
|
||||||
{% elif issubclass(option, Options.ItemDict) and option.verify_item_name %}
|
{% elif issubclass(option, Options.OptionCounter) and (
|
||||||
{{ inputs.ItemDict(option_name, option) }}
|
option.valid_keys or option.verify_item_name or option.verify_location_name
|
||||||
|
) %}
|
||||||
|
{{ inputs.OptionCounter(option_name, option) }}
|
||||||
|
|
||||||
{% elif issubclass(option, Options.OptionList) and option.valid_keys %}
|
{% elif issubclass(option, Options.OptionList) and option.valid_keys %}
|
||||||
{{ inputs.OptionList(option_name, option) }}
|
{{ inputs.OptionList(option_name, option) }}
|
||||||
@@ -133,8 +135,10 @@
|
|||||||
{% elif issubclass(option, Options.FreeText) %}
|
{% elif issubclass(option, Options.FreeText) %}
|
||||||
{{ inputs.FreeText(option_name, option) }}
|
{{ inputs.FreeText(option_name, option) }}
|
||||||
|
|
||||||
{% elif issubclass(option, Options.ItemDict) and option.verify_item_name %}
|
{% elif issubclass(option, Options.OptionCounter) and (
|
||||||
{{ inputs.ItemDict(option_name, option) }}
|
option.valid_keys or option.verify_item_name or option.verify_location_name
|
||||||
|
) %}
|
||||||
|
{{ inputs.OptionCounter(option_name, option) }}
|
||||||
|
|
||||||
{% elif issubclass(option, Options.OptionList) and option.valid_keys %}
|
{% elif issubclass(option, Options.OptionList) and option.valid_keys %}
|
||||||
{{ inputs.OptionList(option_name, option) }}
|
{{ inputs.OptionList(option_name, option) }}
|
||||||
|
|||||||
@@ -29,7 +29,8 @@
|
|||||||
<div id="user-content-wrapper" class="markdown">
|
<div id="user-content-wrapper" class="markdown">
|
||||||
<div id="user-content" class="grass-island">
|
<div id="user-content" class="grass-island">
|
||||||
<h1>User Content</h1>
|
<h1>User Content</h1>
|
||||||
Below is a list of all the content you have generated on this site. Rooms and seeds are listed separately.
|
Below is a list of all the content you have generated on this site. Rooms and seeds are listed separately.<br/>
|
||||||
|
Sessions can be saved or synced across devices using the <a href="{{url_for('show_session')}}">Sessions Page.</a>
|
||||||
|
|
||||||
<h2>Your Rooms</h2>
|
<h2>Your Rooms</h2>
|
||||||
{% if rooms %}
|
{% if rooms %}
|
||||||
|
|||||||
@@ -113,9 +113,18 @@
|
|||||||
{{ TextChoice(option_name, option) }}
|
{{ TextChoice(option_name, option) }}
|
||||||
{% endmacro %}
|
{% endmacro %}
|
||||||
|
|
||||||
{% macro ItemDict(option_name, option, world) %}
|
{% macro OptionCounter(option_name, option, world) %}
|
||||||
|
{% set relevant_keys = option.valid_keys %}
|
||||||
|
{% if not relevant_keys %}
|
||||||
|
{% if option.verify_item_name %}
|
||||||
|
{% set relevant_keys = world.item_names %}
|
||||||
|
{% elif option.verify_location_name %}
|
||||||
|
{% set relevant_keys = world.location_names %}
|
||||||
|
{% endif %}
|
||||||
|
{% endif %}
|
||||||
|
|
||||||
<div class="dict-container">
|
<div class="dict-container">
|
||||||
{% for item_name in (option.valid_keys|sort if (option.valid_keys|length > 0) else world.item_names|sort) %}
|
{% for item_name in (relevant_keys if relevant_keys is ordered else relevant_keys|sort) %}
|
||||||
<div class="dict-entry">
|
<div class="dict-entry">
|
||||||
<label for="{{ option_name }}-{{ item_name }}-qty">{{ item_name }}</label>
|
<label for="{{ option_name }}-{{ item_name }}-qty">{{ item_name }}</label>
|
||||||
<input
|
<input
|
||||||
|
|||||||
@@ -83,8 +83,10 @@
|
|||||||
{% elif issubclass(option, Options.FreeText) %}
|
{% elif issubclass(option, Options.FreeText) %}
|
||||||
{{ inputs.FreeText(option_name, option) }}
|
{{ inputs.FreeText(option_name, option) }}
|
||||||
|
|
||||||
{% elif issubclass(option, Options.ItemDict) and option.verify_item_name %}
|
{% elif issubclass(option, Options.OptionCounter) and (
|
||||||
{{ inputs.ItemDict(option_name, option, world) }}
|
option.valid_keys or option.verify_item_name or option.verify_location_name
|
||||||
|
) %}
|
||||||
|
{{ inputs.OptionCounter(option_name, option, world) }}
|
||||||
|
|
||||||
{% elif issubclass(option, Options.OptionList) and option.valid_keys %}
|
{% elif issubclass(option, Options.OptionList) and option.valid_keys %}
|
||||||
{{ inputs.OptionList(option_name, option) }}
|
{{ inputs.OptionList(option_name, option) }}
|
||||||
|
|||||||
@@ -16,21 +16,30 @@
|
|||||||
orange: "FF7700" # Used for command echo
|
orange: "FF7700" # Used for command echo
|
||||||
# KivyMD theming parameters
|
# KivyMD theming parameters
|
||||||
theme_style: "Dark" # Light/Dark
|
theme_style: "Dark" # Light/Dark
|
||||||
primary_palette: "Green" # Many options
|
primary_palette: "Lightsteelblue" # Many options
|
||||||
dynamic_scheme_name: "TONAL_SPOT"
|
dynamic_scheme_name: "VIBRANT"
|
||||||
dynamic_scheme_contrast: 0.0
|
dynamic_scheme_contrast: 0.0
|
||||||
<MDLabel>:
|
<MDLabel>:
|
||||||
color: self.theme_cls.primaryColor
|
color: self.theme_cls.primaryColor
|
||||||
|
<BaseButton>:
|
||||||
|
ripple_color: app.theme_cls.primaryColor
|
||||||
|
ripple_duration_in_fast: 0.2
|
||||||
|
<MDTabsItemBase>:
|
||||||
|
ripple_color: app.theme_cls.primaryColor
|
||||||
|
ripple_duration_in_fast: 0.2
|
||||||
<TooltipLabel>:
|
<TooltipLabel>:
|
||||||
adaptive_height: True
|
adaptive_height: True
|
||||||
font_size: dp(20)
|
theme_font_size: "Custom"
|
||||||
|
font_size: "20dp"
|
||||||
markup: True
|
markup: True
|
||||||
halign: "left"
|
halign: "left"
|
||||||
<SelectableLabel>:
|
<SelectableLabel>:
|
||||||
size_hint: 1, None
|
size_hint: 1, None
|
||||||
|
theme_text_color: "Custom"
|
||||||
|
text_color: 1, 1, 1, 1
|
||||||
canvas.before:
|
canvas.before:
|
||||||
Color:
|
Color:
|
||||||
rgba: (.0, 0.9, .1, .3) if self.selected else self.theme_cls.surfaceContainerLowColor
|
rgba: (self.theme_cls.primaryColor[0], self.theme_cls.primaryColor[1], self.theme_cls.primaryColor[2], .3) if self.selected else self.theme_cls.surfaceContainerLowestColor
|
||||||
Rectangle:
|
Rectangle:
|
||||||
size: self.size
|
size: self.size
|
||||||
pos: self.pos
|
pos: self.pos
|
||||||
@@ -154,9 +163,12 @@
|
|||||||
<ToolTip>:
|
<ToolTip>:
|
||||||
size: self.texture_size
|
size: self.texture_size
|
||||||
size_hint: None, None
|
size_hint: None, None
|
||||||
|
theme_font_size: "Custom"
|
||||||
font_size: dp(18)
|
font_size: dp(18)
|
||||||
pos_hint: {'center_y': 0.5, 'center_x': 0.5}
|
pos_hint: {'center_y': 0.5, 'center_x': 0.5}
|
||||||
halign: "left"
|
halign: "left"
|
||||||
|
theme_text_color: "Custom"
|
||||||
|
text_color: (1, 1, 1, 1)
|
||||||
canvas.before:
|
canvas.before:
|
||||||
Color:
|
Color:
|
||||||
rgba: 0.2, 0.2, 0.2, 1
|
rgba: 0.2, 0.2, 0.2, 1
|
||||||
@@ -175,11 +187,28 @@
|
|||||||
rectangle: self.x-2, self.y-2, self.width+4, self.height+4
|
rectangle: self.x-2, self.y-2, self.width+4, self.height+4
|
||||||
<ServerToolTip>:
|
<ServerToolTip>:
|
||||||
pos_hint: {'center_y': 0.5, 'center_x': 0.5}
|
pos_hint: {'center_y': 0.5, 'center_x': 0.5}
|
||||||
<AutocompleteHintInput>
|
<AutocompleteHintInput>:
|
||||||
size_hint_y: None
|
size_hint_y: None
|
||||||
height: dp(30)
|
height: "30dp"
|
||||||
multiline: False
|
multiline: False
|
||||||
write_tab: False
|
write_tab: False
|
||||||
|
pos_hint: {"center_x": 0.5, "center_y": 0.5}
|
||||||
|
<ConnectBarTextInput>:
|
||||||
|
height: "30dp"
|
||||||
|
multiline: False
|
||||||
|
write_tab: False
|
||||||
|
role: "medium"
|
||||||
|
size_hint_y: None
|
||||||
|
pos_hint: {"center_x": 0.5, "center_y": 0.5}
|
||||||
|
<CommandPromptTextInput>:
|
||||||
|
size_hint_y: None
|
||||||
|
height: "30dp"
|
||||||
|
multiline: False
|
||||||
|
write_tab: False
|
||||||
|
pos_hint: {"center_x": 0.5, "center_y": 0.5}
|
||||||
|
<MessageBoxLabel>:
|
||||||
|
theme_text_color: "Custom"
|
||||||
|
text_color: 1, 1, 1, 1
|
||||||
<ScrollBox>:
|
<ScrollBox>:
|
||||||
layout: layout
|
layout: layout
|
||||||
bar_width: "12dp"
|
bar_width: "12dp"
|
||||||
|
|||||||
@@ -5,12 +5,13 @@
|
|||||||
size_hint: 1, None
|
size_hint: 1, None
|
||||||
height: "75dp"
|
height: "75dp"
|
||||||
context_button: context
|
context_button: context
|
||||||
|
focus_behavior: False
|
||||||
|
|
||||||
MDRelativeLayout:
|
MDRelativeLayout:
|
||||||
ApAsyncImage:
|
ApAsyncImage:
|
||||||
source: main.image
|
source: main.image
|
||||||
size: (48, 48)
|
size: (48, 48)
|
||||||
size_hint_y: None
|
size_hint: None, None
|
||||||
pos_hint: {"center_x": 0.1, "center_y": 0.5}
|
pos_hint: {"center_x": 0.1, "center_y": 0.5}
|
||||||
|
|
||||||
MDLabel:
|
MDLabel:
|
||||||
@@ -37,6 +38,7 @@
|
|||||||
pos_hint:{"center_x": 0.85, "center_y": 0.8}
|
pos_hint:{"center_x": 0.85, "center_y": 0.8}
|
||||||
theme_text_color: "Custom"
|
theme_text_color: "Custom"
|
||||||
text_color: app.theme_cls.primaryColor
|
text_color: app.theme_cls.primaryColor
|
||||||
|
detect_visible: False
|
||||||
on_release: app.set_favorite(self)
|
on_release: app.set_favorite(self)
|
||||||
|
|
||||||
MDIconButton:
|
MDIconButton:
|
||||||
@@ -46,6 +48,7 @@
|
|||||||
pos_hint:{"center_x": 0.95, "center_y": 0.8}
|
pos_hint:{"center_x": 0.95, "center_y": 0.8}
|
||||||
theme_text_color: "Custom"
|
theme_text_color: "Custom"
|
||||||
text_color: app.theme_cls.primaryColor
|
text_color: app.theme_cls.primaryColor
|
||||||
|
detect_visible: False
|
||||||
|
|
||||||
MDButton:
|
MDButton:
|
||||||
pos_hint:{"center_x": 0.9, "center_y": 0.25}
|
pos_hint:{"center_x": 0.9, "center_y": 0.25}
|
||||||
@@ -53,7 +56,7 @@
|
|||||||
height: "25dp"
|
height: "25dp"
|
||||||
component: main.component
|
component: main.component
|
||||||
on_release: app.component_action(self)
|
on_release: app.component_action(self)
|
||||||
|
detect_visible: False
|
||||||
MDButtonText:
|
MDButtonText:
|
||||||
text: "Open"
|
text: "Open"
|
||||||
|
|
||||||
@@ -77,7 +80,7 @@ MDFloatLayout:
|
|||||||
id: all
|
id: all
|
||||||
style: "text"
|
style: "text"
|
||||||
type: (Type.CLIENT, Type.TOOL, Type.ADJUSTER, Type.MISC)
|
type: (Type.CLIENT, Type.TOOL, Type.ADJUSTER, Type.MISC)
|
||||||
on_release: app.filter_clients(self)
|
on_release: app.filter_clients_by_type(self)
|
||||||
|
|
||||||
MDButtonIcon:
|
MDButtonIcon:
|
||||||
icon: "asterisk"
|
icon: "asterisk"
|
||||||
@@ -87,7 +90,7 @@ MDFloatLayout:
|
|||||||
id: client
|
id: client
|
||||||
style: "text"
|
style: "text"
|
||||||
type: (Type.CLIENT, )
|
type: (Type.CLIENT, )
|
||||||
on_release: app.filter_clients(self)
|
on_release: app.filter_clients_by_type(self)
|
||||||
|
|
||||||
MDButtonIcon:
|
MDButtonIcon:
|
||||||
icon: "controller"
|
icon: "controller"
|
||||||
@@ -97,7 +100,7 @@ MDFloatLayout:
|
|||||||
id: Tool
|
id: Tool
|
||||||
style: "text"
|
style: "text"
|
||||||
type: (Type.TOOL, )
|
type: (Type.TOOL, )
|
||||||
on_release: app.filter_clients(self)
|
on_release: app.filter_clients_by_type(self)
|
||||||
|
|
||||||
MDButtonIcon:
|
MDButtonIcon:
|
||||||
icon: "desktop-classic"
|
icon: "desktop-classic"
|
||||||
@@ -107,7 +110,7 @@ MDFloatLayout:
|
|||||||
id: adjuster
|
id: adjuster
|
||||||
style: "text"
|
style: "text"
|
||||||
type: (Type.ADJUSTER, )
|
type: (Type.ADJUSTER, )
|
||||||
on_release: app.filter_clients(self)
|
on_release: app.filter_clients_by_type(self)
|
||||||
|
|
||||||
MDButtonIcon:
|
MDButtonIcon:
|
||||||
icon: "wrench"
|
icon: "wrench"
|
||||||
@@ -117,7 +120,7 @@ MDFloatLayout:
|
|||||||
id: misc
|
id: misc
|
||||||
style: "text"
|
style: "text"
|
||||||
type: (Type.MISC, )
|
type: (Type.MISC, )
|
||||||
on_release: app.filter_clients(self)
|
on_release: app.filter_clients_by_type(self)
|
||||||
|
|
||||||
MDButtonIcon:
|
MDButtonIcon:
|
||||||
icon: "dots-horizontal-circle-outline"
|
icon: "dots-horizontal-circle-outline"
|
||||||
@@ -128,7 +131,7 @@ MDFloatLayout:
|
|||||||
id: favorites
|
id: favorites
|
||||||
style: "text"
|
style: "text"
|
||||||
type: ("favorites", )
|
type: ("favorites", )
|
||||||
on_release: app.filter_clients(self)
|
on_release: app.filter_clients_by_type(self)
|
||||||
|
|
||||||
MDButtonIcon:
|
MDButtonIcon:
|
||||||
icon: "star"
|
icon: "star"
|
||||||
@@ -138,5 +141,21 @@ MDFloatLayout:
|
|||||||
MDNavigationDrawerDivider:
|
MDNavigationDrawerDivider:
|
||||||
|
|
||||||
|
|
||||||
ScrollBox:
|
MDGridLayout:
|
||||||
id: button_layout
|
id: main_layout
|
||||||
|
cols: 1
|
||||||
|
spacing: "10dp"
|
||||||
|
|
||||||
|
MDTextField:
|
||||||
|
id: search_box
|
||||||
|
mode: "outlined"
|
||||||
|
set_text: app.filter_clients_by_name
|
||||||
|
|
||||||
|
MDTextFieldLeadingIcon:
|
||||||
|
icon: "magnify"
|
||||||
|
|
||||||
|
MDTextFieldHintText:
|
||||||
|
text: "Search"
|
||||||
|
|
||||||
|
ScrollBox:
|
||||||
|
id: button_layout
|
||||||
|
|||||||
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@@ -184,9 +184,6 @@
|
|||||||
# Secret of Evermore
|
# Secret of Evermore
|
||||||
/worlds/soe/ @black-sliver
|
/worlds/soe/ @black-sliver
|
||||||
|
|
||||||
# Slay the Spire
|
|
||||||
/worlds/spire/ @KonoTyran
|
|
||||||
|
|
||||||
# Stardew Valley
|
# Stardew Valley
|
||||||
/worlds/stardew_valley/ @agilbert1412
|
/worlds/stardew_valley/ @agilbert1412
|
||||||
|
|
||||||
@@ -232,10 +229,6 @@
|
|||||||
# Zillion
|
# Zillion
|
||||||
/worlds/zillion/ @beauxq
|
/worlds/zillion/ @beauxq
|
||||||
|
|
||||||
# Zork Grand Inquisitor
|
|
||||||
/worlds/zork_grand_inquisitor/ @nbrochu
|
|
||||||
|
|
||||||
|
|
||||||
## Active Unmaintained Worlds
|
## Active Unmaintained Worlds
|
||||||
|
|
||||||
# The following worlds in this repo are currently unmaintained, but currently still work in core. If any update breaks
|
# The following worlds in this repo are currently unmaintained, but currently still work in core. If any update breaks
|
||||||
|
|||||||
@@ -8,7 +8,11 @@ including [Contributing](contributing.md), [Adding Games](<adding games.md>), an
|
|||||||
|
|
||||||
### My game has a restrictive start that leads to fill errors
|
### My game has a restrictive start that leads to fill errors
|
||||||
|
|
||||||
Hint to the Generator that an item needs to be in sphere one with local_early_items. Here, `1` represents the number of "Sword" items to attempt to place in sphere one.
|
A "restrictive start" here means having a combination of very few sphere 1 locations and potentially requiring more
|
||||||
|
than one item to get a player to sphere 2.
|
||||||
|
|
||||||
|
One way to fix this is to hint to the Generator that an item needs to be in sphere one with local_early_items.
|
||||||
|
Here, `1` represents the number of "Sword" items the Generator will attempt to place in sphere one.
|
||||||
```py
|
```py
|
||||||
early_item_name = "Sword"
|
early_item_name = "Sword"
|
||||||
self.multiworld.local_early_items[self.player][early_item_name] = 1
|
self.multiworld.local_early_items[self.player][early_item_name] = 1
|
||||||
@@ -18,15 +22,19 @@ Some alternative ways to try to fix this problem are:
|
|||||||
* Add more locations to sphere one of your world, potentially only when there would be a restrictive start
|
* Add more locations to sphere one of your world, potentially only when there would be a restrictive start
|
||||||
* Pre-place items yourself, such as during `create_items`
|
* Pre-place items yourself, such as during `create_items`
|
||||||
* Put items into the player's starting inventory using `push_precollected`
|
* Put items into the player's starting inventory using `push_precollected`
|
||||||
* Raise an exception, such as an `OptionError` during `generate_early`, to disallow options that would lead to a restrictive start
|
* Raise an exception, such as an `OptionError` during `generate_early`, to disallow options that would lead to a
|
||||||
|
restrictive start
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
### I have multiple settings that change the item/location pool counts and need to balance them out
|
### I have multiple options that change the item/location pool counts and need to make sure I am not submitting more/fewer items than locations
|
||||||
|
|
||||||
In an ideal situation your system for producing locations and items wouldn't leave any opportunity for them to be unbalanced. But in real, complex situations, that might be unfeasible.
|
In an ideal situation your system for producing locations and items wouldn't leave any opportunity for them to be
|
||||||
|
unbalanced. But in real, complex situations, that might be unfeasible.
|
||||||
|
|
||||||
If that's the case, you can create extra filler based on the difference between your unfilled locations and your itempool by comparing [get_unfilled_locations](https://github.com/ArchipelagoMW/Archipelago/blob/main/BaseClasses.py#:~:text=get_unfilled_locations) to your list of items to submit
|
If that's the case, you can create extra filler based on the difference between your unfilled locations and your
|
||||||
|
itempool by comparing [get_unfilled_locations](https://github.com/ArchipelagoMW/Archipelago/blob/main/BaseClasses.py#:~:text=get_unfilled_locations)
|
||||||
|
to your list of items to submit
|
||||||
|
|
||||||
Note: to use self.create_filler(), self.get_filler_item_name() should be defined to only return valid filler item names
|
Note: to use self.create_filler(), self.get_filler_item_name() should be defined to only return valid filler item names
|
||||||
```py
|
```py
|
||||||
@@ -39,7 +47,8 @@ for _ in range(total_locations - len(item_pool)):
|
|||||||
self.multiworld.itempool += item_pool
|
self.multiworld.itempool += item_pool
|
||||||
```
|
```
|
||||||
|
|
||||||
A faster alternative to the `for` loop would be to use a [list comprehension](https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions):
|
A faster alternative to the `for` loop would be to use a
|
||||||
|
[list comprehension](https://docs.python.org/3/tutorial/datastructures.html#list-comprehensions):
|
||||||
```py
|
```py
|
||||||
item_pool += [self.create_filler() for _ in range(total_locations - len(item_pool))]
|
item_pool += [self.create_filler() for _ in range(total_locations - len(item_pool))]
|
||||||
```
|
```
|
||||||
@@ -48,24 +57,39 @@ item_pool += [self.create_filler() for _ in range(total_locations - len(item_poo
|
|||||||
|
|
||||||
### I learned about indirect conditions in the world API document, but I want to know more. What are they and why are they necessary?
|
### I learned about indirect conditions in the world API document, but I want to know more. What are they and why are they necessary?
|
||||||
|
|
||||||
The world API document mentions how to use `multiworld.register_indirect_condition` to register indirect conditions and **when** you should use them, but not *how* they work and *why* they are necessary. This is because the explanation is quite complicated.
|
The world API document mentions how to use `multiworld.register_indirect_condition` to register indirect conditions and
|
||||||
|
**when** you should use them, but not *how* they work and *why* they are necessary. This is because the explanation is
|
||||||
|
quite complicated.
|
||||||
|
|
||||||
Region sweep (the algorithm that determines which regions are reachable) is a Breadth-First Search of the region graph. It starts from the origin region, checks entrances one by one, and adds newly reached regions and their entrances to the queue until there is nothing more to check.
|
Region sweep (the algorithm that determines which regions are reachable) is a Breadth-First Search of the region graph.
|
||||||
|
It starts from the origin region, checks entrances one by one, and adds newly reached regions and their entrances to
|
||||||
|
the queue until there is nothing more to check.
|
||||||
|
|
||||||
For performance reasons, AP only checks every entrance once. However, if an entrance's access_rule depends on region access, then the following may happen:
|
For performance reasons, AP only checks every entrance once. However, if an entrance's access_rule depends on region
|
||||||
1. The entrance is checked and determined to be nontraversable because the region in its access_rule hasn't been reached yet during the graph search.
|
access, then the following may happen:
|
||||||
|
1. The entrance is checked and determined to be nontraversable because the region in its access_rule hasn't been
|
||||||
|
reached yet during the graph search.
|
||||||
2. Then, the region in its access_rule is determined to be reachable.
|
2. Then, the region in its access_rule is determined to be reachable.
|
||||||
|
|
||||||
This entrance *would* be in logic if it were rechecked, but it won't be rechecked this cycle.
|
This entrance *would* be in logic if it were rechecked, but it won't be rechecked this cycle.
|
||||||
To account for this case, AP would have to recheck all entrances every time a new region is reached until no new regions are reached.
|
To account for this case, AP would have to recheck all entrances every time a new region is reached until no new
|
||||||
|
regions are reached.
|
||||||
|
|
||||||
An indirect condition is how you can manually define that a specific entrance needs to be rechecked during region sweep if a specific region is reached during it.
|
An indirect condition is how you can manually define that a specific entrance needs to be rechecked during region sweep
|
||||||
This keeps most of the performance upsides. Even in a game making heavy use of indirect conditions (ex: The Witness), using them is significantly faster than just "rechecking each entrance until nothing new is found".
|
if a specific region is reached during it.
|
||||||
The reason entrance access rules using `location.can_reach` and `entrance.can_reach` are also affected is because they call `region.can_reach` on their respective parent/source region.
|
This keeps most of the performance upsides. Even in a game making heavy use of indirect conditions (ex: The Witness),
|
||||||
|
using them is significantly faster than just "rechecking each entrance until nothing new is found".
|
||||||
|
The reason entrance access rules using `location.can_reach` and `entrance.can_reach` are also affected is because they
|
||||||
|
call `region.can_reach` on their respective parent/source region.
|
||||||
|
|
||||||
We recognize it can feel like a trap since it will not alert you when you are missing an indirect condition, and that some games have very complex access rules.
|
We recognize it can feel like a trap since it will not alert you when you are missing an indirect condition,
|
||||||
As of [PR #3682 (Core: Region handling customization)](https://github.com/ArchipelagoMW/Archipelago/pull/3682) being merged, it is possible for a world to opt out of indirect conditions entirely, instead using the system of checking each entrance whenever a region has been reached, although this does come with a performance cost.
|
and that some games have very complex access rules.
|
||||||
Opting out of using indirect conditions should only be used by games that *really* need it. For most games, it should be reasonable to know all entrance → region dependencies, making indirect conditions preferred because they are much faster.
|
As of [PR #3682 (Core: Region handling customization)](https://github.com/ArchipelagoMW/Archipelago/pull/3682)
|
||||||
|
being merged, it is possible for a world to opt out of indirect conditions entirely, instead using the system of
|
||||||
|
checking each entrance whenever a region has been reached, although this does come with a performance cost.
|
||||||
|
Opting out of using indirect conditions should only be used by games that *really* need it. For most games, it should
|
||||||
|
be reasonable to know all entrance → region dependencies, making indirect conditions preferred because they are
|
||||||
|
much faster.
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
@@ -85,3 +109,16 @@ Common situations where this can happen include:
|
|||||||
Also, consider using the `options.as_dict("option_name", "option_two")` helper.
|
Also, consider using the `options.as_dict("option_name", "option_two")` helper.
|
||||||
* Using enums as Location/Item names in the datapackage. When building out `location_name_to_id` and `item_name_to_id`,
|
* Using enums as Location/Item names in the datapackage. When building out `location_name_to_id` and `item_name_to_id`,
|
||||||
make sure that you are not using your enum class for either the names or ids in these mappings.
|
make sure that you are not using your enum class for either the names or ids in these mappings.
|
||||||
|
|
||||||
|
---
|
||||||
|
|
||||||
|
### Some locations are technically possible to check with few or no items, but they'd be very tedious or frustrating. How do worlds deal with this?
|
||||||
|
|
||||||
|
Sometimes the game can be modded to skip these locations or make them less tedious. But when this issue is due to a fundamental aspect of the game, then the general answer is "soft logic" (and its subtypes like "combat logic", "money logic", etc.). For example: you can logically require that a player have several helpful items before fighting the final boss, even if a skilled player technically needs no items to beat it. Randomizer logic should describe what's *fun* rather than what's technically possible.
|
||||||
|
|
||||||
|
Concrete examples of soft logic include:
|
||||||
|
- Defeating a boss might logically require health upgrades, damage upgrades, certain weapons, etc. that aren't strictly necessary.
|
||||||
|
- Entering a high-level area might logically require access to enough other parts of the game that checking other locations should naturally get the player to the soft-required level.
|
||||||
|
- Buying expensive shop items might logically require access to a place where you can quickly farm money, or logically require access to enough parts of the game that checking other locations should naturally generate enough money without grinding.
|
||||||
|
|
||||||
|
Remember that all items referenced by logic (however hard or soft) must be `progression`. Since you typically don't want to turn a ton of `filler` items into `progression` just for this, it's common to e.g. write money logic using only the rare "$100" item, so the dozens of "$1" and "$10" items in your world can remain `filler`.
|
||||||
|
|||||||
@@ -117,8 +117,6 @@ flowchart LR
|
|||||||
%% Java Based Games
|
%% Java Based Games
|
||||||
subgraph Java
|
subgraph Java
|
||||||
JM[Mod with Archipelago.MultiClient.Java]
|
JM[Mod with Archipelago.MultiClient.Java]
|
||||||
STS[Slay the Spire]
|
|
||||||
JM <-- Mod the Spire --> STS
|
|
||||||
subgraph Minecraft
|
subgraph Minecraft
|
||||||
MCS[Minecraft Forge Server]
|
MCS[Minecraft Forge Server]
|
||||||
JMC[Any Java Minecraft Clients]
|
JMC[Any Java Minecraft Clients]
|
||||||
|
|||||||
@@ -352,8 +352,15 @@ template. If you set a [Schema](https://pypi.org/project/schema/) on the class w
|
|||||||
options system will automatically validate the user supplied data against the schema to ensure it's in the correct
|
options system will automatically validate the user supplied data against the schema to ensure it's in the correct
|
||||||
format.
|
format.
|
||||||
|
|
||||||
|
### OptionCounter
|
||||||
|
This is a special case of OptionDict where the dictionary values can only be integers.
|
||||||
|
It returns a [collections.Counter](https://docs.python.org/3/library/collections.html#collections.Counter).
|
||||||
|
This means that if you access a key that isn't present, its value will be 0.
|
||||||
|
The upside of using an OptionCounter (instead of an OptionDict with integer values) is that an OptionCounter can be
|
||||||
|
displayed on the Options page on WebHost.
|
||||||
|
|
||||||
### ItemDict
|
### ItemDict
|
||||||
Like OptionDict, except this will verify that every key in the dictionary is a valid name for an item for your world.
|
An OptionCounter that will verify that every key in the dictionary is a valid name for an item for your world.
|
||||||
|
|
||||||
### OptionList
|
### OptionList
|
||||||
This option defines a List, where the user can add any number of strings to said list, allowing duplicate values. You
|
This option defines a List, where the user can add any number of strings to said list, allowing duplicate values. You
|
||||||
|
|||||||
@@ -561,7 +561,7 @@ from .items import is_progression # this is just a dummy
|
|||||||
|
|
||||||
|
|
||||||
def create_item(self, item: str) -> MyGameItem:
|
def create_item(self, item: str) -> MyGameItem:
|
||||||
# this is called when AP wants to create an item by name (for plando) or when you call it from your own code
|
# this is called when AP wants to create an item by name (for plando, start inventory, item links) or when you call it from your own code
|
||||||
classification = ItemClassification.progression if is_progression(item) else ItemClassification.filler
|
classification = ItemClassification.progression if is_progression(item) else ItemClassification.filler
|
||||||
return MyGameItem(item, classification, self.item_name_to_id[item], self.player)
|
return MyGameItem(item, classification, self.item_name_to_id[item], self.player)
|
||||||
|
|
||||||
|
|||||||
@@ -45,7 +45,8 @@ MinVersion={#min_windows}
|
|||||||
Name: "english"; MessagesFile: "compiler:Default.isl"
|
Name: "english"; MessagesFile: "compiler:Default.isl"
|
||||||
|
|
||||||
[Tasks]
|
[Tasks]
|
||||||
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}"; GroupDescription: "{cm:AdditionalIcons}";
|
Name: "desktopicon"; Description: "{cm:CreateDesktopIcon}";
|
||||||
|
Name: "deletelib"; Description: "Clean existing /lib folder and subfolders including /worlds (leave checked if unsure)"; Check: ShouldShowDeleteLibTask
|
||||||
|
|
||||||
[Types]
|
[Types]
|
||||||
Name: "full"; Description: "Full installation"
|
Name: "full"; Description: "Full installation"
|
||||||
@@ -83,18 +84,8 @@ Filename: "{app}\ArchipelagoLauncher"; Description: "{cm:LaunchProgram,{#StringC
|
|||||||
Type: dirifempty; Name: "{app}"
|
Type: dirifempty; Name: "{app}"
|
||||||
|
|
||||||
[InstallDelete]
|
[InstallDelete]
|
||||||
Type: files; Name: "{app}\lib\worlds\_bizhawk.apworld"
|
Type: files; Name: "{app}\*.exe"
|
||||||
Type: files; Name: "{app}\ArchipelagoLttPClient.exe"
|
|
||||||
Type: files; Name: "{app}\ArchipelagoPokemonClient.exe"
|
|
||||||
Type: files; Name: "{app}\data\lua\connector_pkmn_rb.lua"
|
Type: files; Name: "{app}\data\lua\connector_pkmn_rb.lua"
|
||||||
Type: filesandordirs; Name: "{app}\lib\worlds\rogue-legacy"
|
|
||||||
Type: dirifempty; Name: "{app}\lib\worlds\rogue-legacy"
|
|
||||||
Type: files; Name: "{app}\lib\worlds\sc2wol.apworld"
|
|
||||||
Type: filesandordirs; Name: "{app}\lib\worlds\sc2wol"
|
|
||||||
Type: dirifempty; Name: "{app}\lib\worlds\sc2wol"
|
|
||||||
Type: filesandordirs; Name: "{app}\lib\worlds\bk_sudoku"
|
|
||||||
Type: dirifempty; Name: "{app}\lib\worlds\bk_sudoku"
|
|
||||||
Type: files; Name: "{app}\ArchipelagoLauncher(DEBUG).exe"
|
|
||||||
Type: filesandordirs; Name: "{app}\SNI\lua*"
|
Type: filesandordirs; Name: "{app}\SNI\lua*"
|
||||||
Type: filesandordirs; Name: "{app}\EnemizerCLI*"
|
Type: filesandordirs; Name: "{app}\EnemizerCLI*"
|
||||||
#include "installdelete.iss"
|
#include "installdelete.iss"
|
||||||
@@ -261,3 +252,17 @@ begin
|
|||||||
Result := True;
|
Result := True;
|
||||||
end;
|
end;
|
||||||
end;
|
end;
|
||||||
|
|
||||||
|
function ShouldShowDeleteLibTask: Boolean;
|
||||||
|
begin
|
||||||
|
Result := DirExists(ExpandConstant('{app}\lib'));
|
||||||
|
end;
|
||||||
|
|
||||||
|
procedure CurStepChanged(CurStep: TSetupStep);
|
||||||
|
begin
|
||||||
|
if CurStep = ssInstall then
|
||||||
|
begin
|
||||||
|
if WizardIsTaskSelected('deletelib') then
|
||||||
|
DelTree(ExpandConstant('{app}\lib'), True, True, True);
|
||||||
|
end;
|
||||||
|
end;
|
||||||
|
|||||||
197
kvui.py
197
kvui.py
@@ -43,8 +43,8 @@ from kivy.core.image import ImageLoader, ImageLoaderBase, ImageData
|
|||||||
from kivy.base import ExceptionHandler, ExceptionManager
|
from kivy.base import ExceptionHandler, ExceptionManager
|
||||||
from kivy.clock import Clock
|
from kivy.clock import Clock
|
||||||
from kivy.factory import Factory
|
from kivy.factory import Factory
|
||||||
from kivy.properties import BooleanProperty, ObjectProperty, NumericProperty
|
from kivy.properties import BooleanProperty, ObjectProperty, NumericProperty, StringProperty
|
||||||
from kivy.metrics import dp
|
from kivy.metrics import dp, sp
|
||||||
from kivy.uix.widget import Widget
|
from kivy.uix.widget import Widget
|
||||||
from kivy.uix.layout import Layout
|
from kivy.uix.layout import Layout
|
||||||
from kivy.utils import escape_markup
|
from kivy.utils import escape_markup
|
||||||
@@ -60,7 +60,7 @@ from kivymd.app import MDApp
|
|||||||
from kivymd.uix.gridlayout import MDGridLayout
|
from kivymd.uix.gridlayout import MDGridLayout
|
||||||
from kivymd.uix.floatlayout import MDFloatLayout
|
from kivymd.uix.floatlayout import MDFloatLayout
|
||||||
from kivymd.uix.boxlayout import MDBoxLayout
|
from kivymd.uix.boxlayout import MDBoxLayout
|
||||||
from kivymd.uix.tab.tab import MDTabsPrimary, MDTabsItem, MDTabsItemText, MDTabsCarousel
|
from kivymd.uix.tab.tab import MDTabsSecondary, MDTabsItem, MDTabsItemText, MDTabsCarousel
|
||||||
from kivymd.uix.menu import MDDropdownMenu
|
from kivymd.uix.menu import MDDropdownMenu
|
||||||
from kivymd.uix.menu.menu import MDDropdownTextItem
|
from kivymd.uix.menu.menu import MDDropdownTextItem
|
||||||
from kivymd.uix.dropdownitem import MDDropDownItem, MDDropDownItemText
|
from kivymd.uix.dropdownitem import MDDropDownItem, MDDropDownItemText
|
||||||
@@ -90,15 +90,15 @@ remove_between_brackets = re.compile(r"\[.*?]")
|
|||||||
class ThemedApp(MDApp):
|
class ThemedApp(MDApp):
|
||||||
def set_colors(self):
|
def set_colors(self):
|
||||||
text_colors = KivyJSONtoTextParser.TextColors()
|
text_colors = KivyJSONtoTextParser.TextColors()
|
||||||
self.theme_cls.theme_style = getattr(text_colors, "theme_style", "Dark")
|
self.theme_cls.theme_style = text_colors.theme_style
|
||||||
self.theme_cls.primary_palette = getattr(text_colors, "primary_palette", "Green")
|
self.theme_cls.primary_palette = text_colors.primary_palette
|
||||||
self.theme_cls.dynamic_scheme_name = getattr(text_colors, "dynamic_scheme_name", "TONAL_SPOT")
|
self.theme_cls.dynamic_scheme_name = text_colors.dynamic_scheme_name
|
||||||
self.theme_cls.dynamic_scheme_contrast = getattr(text_colors, "dynamic_scheme_contrast", 0.0)
|
self.theme_cls.dynamic_scheme_contrast = text_colors.dynamic_scheme_contrast
|
||||||
|
|
||||||
|
|
||||||
class ImageIcon(MDButtonIcon, AsyncImage):
|
class ImageIcon(MDButtonIcon, AsyncImage):
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(args, kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
self.image = ApAsyncImage(**kwargs)
|
self.image = ApAsyncImage(**kwargs)
|
||||||
self.add_widget(self.image)
|
self.add_widget(self.image)
|
||||||
|
|
||||||
@@ -166,6 +166,34 @@ class ToggleButton(MDButton, ToggleButtonBehavior):
|
|||||||
child.icon_color = self.theme_cls.primaryColor
|
child.icon_color = self.theme_cls.primaryColor
|
||||||
|
|
||||||
|
|
||||||
|
# thanks kivymd
|
||||||
|
class ResizableTextField(MDTextField):
|
||||||
|
"""
|
||||||
|
Resizable MDTextField that manually overrides the builtin sizing.
|
||||||
|
|
||||||
|
Note that in order to use this, the sizing must be specified from within a .kv rule.
|
||||||
|
"""
|
||||||
|
def __init__(self, *args, **kwargs):
|
||||||
|
# cursed rules override
|
||||||
|
rules = Builder.match(self)
|
||||||
|
textfield = next((rule for rule in rules if rule.name == f"<MDTextField>"), None)
|
||||||
|
if textfield:
|
||||||
|
subclasses = rules[rules.index(textfield) + 1:]
|
||||||
|
for subclass in subclasses:
|
||||||
|
height_rule = subclass.properties.get("height", None)
|
||||||
|
if height_rule:
|
||||||
|
height_rule.ignore_prev = True
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
|
||||||
|
|
||||||
|
def on_release(self: MDButton, *args):
|
||||||
|
super(MDButton, self).on_release(args)
|
||||||
|
self.on_leave()
|
||||||
|
|
||||||
|
|
||||||
|
MDButton.on_release = on_release
|
||||||
|
|
||||||
|
|
||||||
# I was surprised to find this didn't already exist in kivy :(
|
# I was surprised to find this didn't already exist in kivy :(
|
||||||
class HoverBehavior(object):
|
class HoverBehavior(object):
|
||||||
"""originally from https://stackoverflow.com/a/605348110"""
|
"""originally from https://stackoverflow.com/a/605348110"""
|
||||||
@@ -266,11 +294,15 @@ class TooltipLabel(HovererableLabel, MDTooltip):
|
|||||||
self._tooltip = None
|
self._tooltip = None
|
||||||
|
|
||||||
|
|
||||||
class ServerLabel(HovererableLabel, MDTooltip):
|
class ServerLabel(HoverBehavior, MDTooltip, MDBoxLayout):
|
||||||
tooltip_display_delay = 0.1
|
tooltip_display_delay = 0.1
|
||||||
|
text: str = StringProperty("Server:")
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super(HovererableLabel, self).__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
|
self.add_widget(MDIcon(icon="information", font_size=sp(15)))
|
||||||
|
self.add_widget(TooltipLabel(text=self.text, pos_hint={"center_x": 0.5, "center_y": 0.5},
|
||||||
|
font_size=sp(15)))
|
||||||
self._tooltip = ServerToolTip(text="Test")
|
self._tooltip = ServerToolTip(text="Test")
|
||||||
|
|
||||||
def on_enter(self):
|
def on_enter(self):
|
||||||
@@ -383,7 +415,6 @@ class MarkupDropdownTextItem(MDDropdownTextItem):
|
|||||||
for child in self.children:
|
for child in self.children:
|
||||||
if child.__class__ == MDLabel:
|
if child.__class__ == MDLabel:
|
||||||
child.markup = True
|
child.markup = True
|
||||||
print(self.text)
|
|
||||||
# Currently, this only lets us do markup on text that does not have any icons
|
# Currently, this only lets us do markup on text that does not have any icons
|
||||||
# Create new TextItems as needed
|
# Create new TextItems as needed
|
||||||
|
|
||||||
@@ -461,14 +492,13 @@ class MarkupDropdown(MDDropdownMenu):
|
|||||||
self.menu.data = self._items
|
self.menu.data = self._items
|
||||||
|
|
||||||
|
|
||||||
class AutocompleteHintInput(MDTextField):
|
class AutocompleteHintInput(ResizableTextField):
|
||||||
min_chars = NumericProperty(3)
|
min_chars = NumericProperty(3)
|
||||||
|
|
||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
|
|
||||||
self.dropdown = MarkupDropdown(caller=self, position="bottom", border_margin=dp(24), width=self.width)
|
self.dropdown = MarkupDropdown(caller=self, position="bottom", border_margin=dp(2), width=self.width)
|
||||||
self.dropdown.bind(on_select=lambda instance, x: setattr(self, 'text', x))
|
|
||||||
self.bind(on_text_validate=self.on_message)
|
self.bind(on_text_validate=self.on_message)
|
||||||
self.bind(width=lambda instance, x: setattr(self.dropdown, "width", x))
|
self.bind(width=lambda instance, x: setattr(self.dropdown, "width", x))
|
||||||
|
|
||||||
@@ -485,8 +515,11 @@ class AutocompleteHintInput(MDTextField):
|
|||||||
|
|
||||||
def on_press(text):
|
def on_press(text):
|
||||||
split_text = MarkupLabel(text=text).markup
|
split_text = MarkupLabel(text=text).markup
|
||||||
return self.dropdown.select("".join(text_frag for text_frag in split_text
|
self.set_text(self, "".join(text_frag for text_frag in split_text
|
||||||
if not text_frag.startswith("[")))
|
if not text_frag.startswith("[")))
|
||||||
|
self.dropdown.dismiss()
|
||||||
|
self.focus = True
|
||||||
|
|
||||||
lowered = value.lower()
|
lowered = value.lower()
|
||||||
for item_name in item_names:
|
for item_name in item_names:
|
||||||
try:
|
try:
|
||||||
@@ -498,7 +531,7 @@ class AutocompleteHintInput(MDTextField):
|
|||||||
text = text[:index] + "[b]" + text[index:index+len(value)]+"[/b]"+text[index+len(value):]
|
text = text[:index] + "[b]" + text[index:index+len(value)]+"[/b]"+text[index+len(value):]
|
||||||
self.dropdown.items.append({
|
self.dropdown.items.append({
|
||||||
"text": text,
|
"text": text,
|
||||||
"on_release": lambda: on_press(text),
|
"on_release": lambda txt=text: on_press(txt),
|
||||||
"markup": True
|
"markup": True
|
||||||
})
|
})
|
||||||
if not self.dropdown.parent:
|
if not self.dropdown.parent:
|
||||||
@@ -620,7 +653,7 @@ class HintLabel(RecycleDataViewBehavior, MDBoxLayout):
|
|||||||
self.selected = is_selected
|
self.selected = is_selected
|
||||||
|
|
||||||
|
|
||||||
class ConnectBarTextInput(MDTextField):
|
class ConnectBarTextInput(ResizableTextField):
|
||||||
def insert_text(self, substring, from_undo=False):
|
def insert_text(self, substring, from_undo=False):
|
||||||
s = substring.replace("\n", "").replace("\r", "")
|
s = substring.replace("\n", "").replace("\r", "")
|
||||||
return super(ConnectBarTextInput, self).insert_text(s, from_undo=from_undo)
|
return super(ConnectBarTextInput, self).insert_text(s, from_undo=from_undo)
|
||||||
@@ -630,7 +663,7 @@ def is_command_input(string: str) -> bool:
|
|||||||
return len(string) > 0 and string[0] in "/!"
|
return len(string) > 0 and string[0] in "/!"
|
||||||
|
|
||||||
|
|
||||||
class CommandPromptTextInput(MDTextField):
|
class CommandPromptTextInput(ResizableTextField):
|
||||||
MAXIMUM_HISTORY_MESSAGES = 50
|
MAXIMUM_HISTORY_MESSAGES = 50
|
||||||
|
|
||||||
def __init__(self, **kwargs) -> None:
|
def __init__(self, **kwargs) -> None:
|
||||||
@@ -682,29 +715,61 @@ class MessageBox(Popup):
|
|||||||
def __init__(self, **kwargs):
|
def __init__(self, **kwargs):
|
||||||
super().__init__(**kwargs)
|
super().__init__(**kwargs)
|
||||||
self._label.refresh()
|
self._label.refresh()
|
||||||
self.size = self._label.texture.size
|
|
||||||
if self.width + 50 > Window.width:
|
|
||||||
self.text_size[0] = Window.width - 50
|
|
||||||
self._label.refresh()
|
|
||||||
self.size = self._label.texture.size
|
|
||||||
|
|
||||||
def __init__(self, title, text, error=False, **kwargs):
|
def __init__(self, title, text, error=False, **kwargs):
|
||||||
label = MessageBox.MessageBoxLabel(text=text)
|
label = MessageBox.MessageBoxLabel(text=text)
|
||||||
separator_color = [217 / 255, 129 / 255, 122 / 255, 1.] if error else [47 / 255., 167 / 255., 212 / 255, 1.]
|
separator_color = [217 / 255, 129 / 255, 122 / 255, 1.] if error else [47 / 255., 167 / 255., 212 / 255, 1.]
|
||||||
super().__init__(title=title, content=label, size_hint=(None, None), width=max(100, int(label.width) + 40),
|
super().__init__(title=title, content=label, size_hint=(0.5, None), width=max(100, int(label.width) + 40),
|
||||||
separator_color=separator_color, **kwargs)
|
separator_color=separator_color, **kwargs)
|
||||||
self.height += max(0, label.height - 18)
|
self.height += max(0, label.height - 18)
|
||||||
|
|
||||||
|
|
||||||
class ClientTabs(MDTabsPrimary):
|
class ClientTabs(MDTabsSecondary):
|
||||||
carousel: MDTabsCarousel
|
carousel: MDTabsCarousel
|
||||||
lock_swiping = True
|
lock_swiping = True
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
self.carousel = MDTabsCarousel(lock_swiping=True)
|
self.carousel = MDTabsCarousel(lock_swiping=True, anim_move_duration=0.2)
|
||||||
super().__init__(*args, MDDivider(size_hint_y=None, height=dp(4)), self.carousel, **kwargs)
|
super().__init__(*args, MDDivider(size_hint_y=None, height=dp(1)), self.carousel, **kwargs)
|
||||||
self.size_hint_y = 1
|
self.size_hint_y = 1
|
||||||
|
|
||||||
|
def _check_panel_height(self, *args):
|
||||||
|
self.ids.tab_scroll.height = dp(38)
|
||||||
|
|
||||||
|
def update_indicator(
|
||||||
|
self, x: float = 0.0, w: float = 0.0, instance: MDTabsItem = None
|
||||||
|
) -> None:
|
||||||
|
def update_indicator(*args):
|
||||||
|
indicator_pos = (0, 0)
|
||||||
|
indicator_size = (0, 0)
|
||||||
|
|
||||||
|
item_text_object = self._get_tab_item_text_icon_object()
|
||||||
|
|
||||||
|
if item_text_object:
|
||||||
|
indicator_pos = (
|
||||||
|
instance.x + dp(12),
|
||||||
|
self.indicator.pos[1]
|
||||||
|
if not self._tabs_carousel
|
||||||
|
else self._tabs_carousel.height,
|
||||||
|
)
|
||||||
|
indicator_size = (
|
||||||
|
instance.width - dp(24),
|
||||||
|
self.indicator_height,
|
||||||
|
)
|
||||||
|
|
||||||
|
Animation(
|
||||||
|
pos=indicator_pos,
|
||||||
|
size=indicator_size,
|
||||||
|
d=0 if not self.indicator_anim else self.indicator_duration,
|
||||||
|
t=self.indicator_transition,
|
||||||
|
).start(self.indicator)
|
||||||
|
|
||||||
|
if not instance:
|
||||||
|
self.indicator.pos = (x, self.indicator.pos[1])
|
||||||
|
self.indicator.size = (w, self.indicator_height)
|
||||||
|
else:
|
||||||
|
Clock.schedule_once(update_indicator)
|
||||||
|
|
||||||
def remove_tab(self, tab, content=None):
|
def remove_tab(self, tab, content=None):
|
||||||
if content is None:
|
if content is None:
|
||||||
content = tab.content
|
content = tab.content
|
||||||
@@ -713,6 +778,21 @@ class ClientTabs(MDTabsPrimary):
|
|||||||
self.on_size(self, self.size)
|
self.on_size(self, self.size)
|
||||||
|
|
||||||
|
|
||||||
|
class CommandButton(MDButton, MDTooltip):
|
||||||
|
def __init__(self, *args, manager: "GameManager", **kwargs):
|
||||||
|
super().__init__(*args, **kwargs)
|
||||||
|
self.manager = manager
|
||||||
|
self._tooltip = ToolTip(text="Test")
|
||||||
|
|
||||||
|
def on_enter(self):
|
||||||
|
self._tooltip.text = self.manager.commandprocessor.get_help_text()
|
||||||
|
self._tooltip.font_size = dp(20 - (len(self._tooltip.text) // 400)) # mostly guessing on the numbers here
|
||||||
|
self.display_tooltip()
|
||||||
|
|
||||||
|
def on_leave(self):
|
||||||
|
self.animation_tooltip_dismiss()
|
||||||
|
|
||||||
|
|
||||||
class GameManager(ThemedApp):
|
class GameManager(ThemedApp):
|
||||||
logging_pairs = [
|
logging_pairs = [
|
||||||
("Client", "Archipelago"),
|
("Client", "Archipelago"),
|
||||||
@@ -767,19 +847,19 @@ class GameManager(ThemedApp):
|
|||||||
|
|
||||||
self.grid = MainLayout()
|
self.grid = MainLayout()
|
||||||
self.grid.cols = 1
|
self.grid.cols = 1
|
||||||
self.connect_layout = MDBoxLayout(orientation="horizontal", size_hint_y=None, height=dp(70),
|
self.connect_layout = MDBoxLayout(orientation="horizontal", size_hint_y=None, height=dp(40),
|
||||||
spacing=5, padding=(5, 10))
|
spacing=5, padding=(5, 10))
|
||||||
# top part
|
# top part
|
||||||
server_label = ServerLabel(halign="center")
|
server_label = ServerLabel(width=dp(75))
|
||||||
self.connect_layout.add_widget(server_label)
|
self.connect_layout.add_widget(server_label)
|
||||||
self.server_connect_bar = ConnectBarTextInput(text=self.ctx.suggested_address or "archipelago.gg:",
|
self.server_connect_bar = ConnectBarTextInput(text=self.ctx.suggested_address or "archipelago.gg:",
|
||||||
size_hint_y=None, role="medium",
|
pos_hint={"center_x": 0.5, "center_y": 0.5})
|
||||||
height=dp(70), multiline=False, write_tab=False)
|
|
||||||
|
|
||||||
def connect_bar_validate(sender):
|
def connect_bar_validate(sender):
|
||||||
if not self.ctx.server:
|
if not self.ctx.server:
|
||||||
self.connect_button_action(sender)
|
self.connect_button_action(sender)
|
||||||
|
|
||||||
|
self.server_connect_bar.height = dp(30)
|
||||||
self.server_connect_bar.bind(on_text_validate=connect_bar_validate)
|
self.server_connect_bar.bind(on_text_validate=connect_bar_validate)
|
||||||
self.connect_layout.add_widget(self.server_connect_bar)
|
self.connect_layout.add_widget(self.server_connect_bar)
|
||||||
self.server_connect_button = MDButton(MDButtonText(text="Connect"), style="filled", size=(dp(100), dp(70)),
|
self.server_connect_button = MDButton(MDButtonText(text="Connect"), style="filled", size=(dp(100), dp(70)),
|
||||||
@@ -792,7 +872,7 @@ class GameManager(ThemedApp):
|
|||||||
self.grid.add_widget(self.progressbar)
|
self.grid.add_widget(self.progressbar)
|
||||||
|
|
||||||
# middle part
|
# middle part
|
||||||
self.tabs = ClientTabs()
|
self.tabs = ClientTabs(pos_hint={"center_x": 0.5, "center_y": 0.5})
|
||||||
self.tabs.add_widget(MDTabsItem(MDTabsItemText(text="All" if len(self.logging_pairs) > 1 else "Archipelago")))
|
self.tabs.add_widget(MDTabsItem(MDTabsItemText(text="All" if len(self.logging_pairs) > 1 else "Archipelago")))
|
||||||
self.log_panels["All"] = self.tabs.default_tab_content = UILog(*(logging.getLogger(logger_name)
|
self.log_panels["All"] = self.tabs.default_tab_content = UILog(*(logging.getLogger(logger_name)
|
||||||
for logger_name, name in
|
for logger_name, name in
|
||||||
@@ -820,12 +900,13 @@ class GameManager(ThemedApp):
|
|||||||
self.grid.add_widget(self.main_area_container)
|
self.grid.add_widget(self.main_area_container)
|
||||||
|
|
||||||
# bottom part
|
# bottom part
|
||||||
bottom_layout = MDBoxLayout(orientation="horizontal", size_hint_y=None, height=dp(70), spacing=5, padding=(5, 10))
|
bottom_layout = MDBoxLayout(orientation="horizontal", size_hint_y=None, height=dp(40), spacing=5, padding=(5, 10))
|
||||||
info_button = MDButton(MDButtonText(text="Command:"), radius=5, style="filled", size=(dp(100), dp(70)),
|
info_button = CommandButton(MDButtonText(text="Command:", halign="left"), manager=self, radius=5,
|
||||||
size_hint_x=None, size_hint_y=None, pos_hint={"center_y": 0.575})
|
style="filled", size=(dp(100), dp(70)), size_hint_x=None, size_hint_y=None,
|
||||||
|
pos_hint={"center_y": 0.575})
|
||||||
info_button.bind(on_release=self.command_button_action)
|
info_button.bind(on_release=self.command_button_action)
|
||||||
bottom_layout.add_widget(info_button)
|
bottom_layout.add_widget(info_button)
|
||||||
self.textinput = CommandPromptTextInput(size_hint_y=None, height=dp(30), multiline=False, write_tab=False)
|
self.textinput = CommandPromptTextInput(size_hint_y=None, multiline=False, write_tab=False)
|
||||||
self.textinput.bind(on_text_validate=self.on_message)
|
self.textinput.bind(on_text_validate=self.on_message)
|
||||||
info_button.height = self.textinput.height
|
info_button.height = self.textinput.height
|
||||||
self.textinput.text_validate_unfocus = False
|
self.textinput.text_validate_unfocus = False
|
||||||
@@ -843,15 +924,27 @@ class GameManager(ThemedApp):
|
|||||||
self.server_connect_bar.focus = True
|
self.server_connect_bar.focus = True
|
||||||
self.server_connect_bar.select_text(port_start if port_start > 0 else host_start, len(s))
|
self.server_connect_bar.select_text(port_start if port_start > 0 else host_start, len(s))
|
||||||
|
|
||||||
|
# Uncomment to enable the kivy live editor console
|
||||||
|
# Press Ctrl-E (with numlock/capslock) disabled to open
|
||||||
|
# from kivy.core.window import Window
|
||||||
|
# from kivy.modules import console
|
||||||
|
# console.create_console(Window, self.container)
|
||||||
|
|
||||||
return self.container
|
return self.container
|
||||||
|
|
||||||
def add_client_tab(self, title: str, content: Widget) -> Widget:
|
def add_client_tab(self, title: str, content: Widget, index: int = -1) -> Widget:
|
||||||
"""Adds a new tab to the client window with a given title, and provides a given Widget as its content.
|
"""Adds a new tab to the client window with a given title, and provides a given Widget as its content.
|
||||||
Returns the new tab widget, with the provided content being placed on the tab as content."""
|
Returns the new tab widget, with the provided content being placed on the tab as content."""
|
||||||
new_tab = MDTabsItem(MDTabsItemText(text=title))
|
new_tab = MDTabsItem(MDTabsItemText(text=title))
|
||||||
new_tab.content = content
|
new_tab.content = content
|
||||||
self.tabs.add_widget(new_tab)
|
if -1 < index <= len(self.tabs.carousel.slides):
|
||||||
self.tabs.carousel.add_widget(new_tab.content)
|
new_tab.bind(on_release=self.tabs.set_active_item)
|
||||||
|
new_tab._tabs = self.tabs
|
||||||
|
self.tabs.ids.container.add_widget(new_tab, index=index)
|
||||||
|
self.tabs.carousel.add_widget(new_tab.content, index=len(self.tabs.carousel.slides) - index)
|
||||||
|
else:
|
||||||
|
self.tabs.add_widget(new_tab)
|
||||||
|
self.tabs.carousel.add_widget(new_tab.content)
|
||||||
return new_tab
|
return new_tab
|
||||||
|
|
||||||
def update_texts(self, dt):
|
def update_texts(self, dt):
|
||||||
@@ -1001,8 +1094,9 @@ class HintLayout(MDBoxLayout):
|
|||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
super().__init__(*args, **kwargs)
|
super().__init__(*args, **kwargs)
|
||||||
boxlayout = MDBoxLayout(orientation="horizontal", size_hint_y=None, height=dp(55))
|
boxlayout = MDBoxLayout(orientation="horizontal", size_hint_y=None, height=dp(40))
|
||||||
boxlayout.add_widget(MDLabel(text="New Hint:", size_hint_x=None, size_hint_y=None, height=dp(55)))
|
boxlayout.add_widget(MDLabel(text="New Hint:", size_hint_x=None, size_hint_y=None,
|
||||||
|
height=dp(40), width=dp(75), halign="center", valign="center"))
|
||||||
boxlayout.add_widget(AutocompleteHintInput())
|
boxlayout.add_widget(AutocompleteHintInput())
|
||||||
self.add_widget(boxlayout)
|
self.add_widget(boxlayout)
|
||||||
|
|
||||||
@@ -1109,6 +1203,7 @@ class HintLog(MDRecycleView):
|
|||||||
|
|
||||||
|
|
||||||
class ApAsyncImage(AsyncImage):
|
class ApAsyncImage(AsyncImage):
|
||||||
|
|
||||||
def is_uri(self, filename: str) -> bool:
|
def is_uri(self, filename: str) -> bool:
|
||||||
if filename.startswith("ap:"):
|
if filename.startswith("ap:"):
|
||||||
return True
|
return True
|
||||||
@@ -1154,7 +1249,23 @@ class E(ExceptionHandler):
|
|||||||
class KivyJSONtoTextParser(JSONtoTextParser):
|
class KivyJSONtoTextParser(JSONtoTextParser):
|
||||||
# dummy class to absorb kvlang definitions
|
# dummy class to absorb kvlang definitions
|
||||||
class TextColors(Widget):
|
class TextColors(Widget):
|
||||||
pass
|
white: str = StringProperty("FFFFFF")
|
||||||
|
black: str = StringProperty("000000")
|
||||||
|
red: str = StringProperty("EE0000")
|
||||||
|
green: str = StringProperty("00FF7F")
|
||||||
|
yellow: str = StringProperty("FAFAD2")
|
||||||
|
blue: str = StringProperty("6495ED")
|
||||||
|
magenta: str = StringProperty("EE00EE")
|
||||||
|
cyan: str = StringProperty("00EEEE")
|
||||||
|
slateblue: str = StringProperty("6D8BE8")
|
||||||
|
plum: str = StringProperty("AF99EF")
|
||||||
|
salmon: str = StringProperty("FA8072")
|
||||||
|
orange: str = StringProperty("FF7700")
|
||||||
|
# KivyMD parameters
|
||||||
|
theme_style: str = StringProperty("Dark")
|
||||||
|
primary_palette: str = StringProperty("Lightsteelblue")
|
||||||
|
dynamic_scheme_name: str = StringProperty("VIBRANT")
|
||||||
|
dynamic_scheme_contrast: int = NumericProperty(0)
|
||||||
|
|
||||||
def __init__(self, *args, **kwargs):
|
def __init__(self, *args, **kwargs):
|
||||||
# we grab the color definitions from the .kv file, then overwrite the JSONtoTextParser default entries
|
# we grab the color definitions from the .kv file, then overwrite the JSONtoTextParser default entries
|
||||||
|
|||||||
@@ -7,7 +7,7 @@ schema>=0.7.7
|
|||||||
kivy>=2.3.1
|
kivy>=2.3.1
|
||||||
bsdiff4>=1.2.6
|
bsdiff4>=1.2.6
|
||||||
platformdirs>=4.3.6
|
platformdirs>=4.3.6
|
||||||
certifi>=2025.1.31
|
certifi>=2025.4.26
|
||||||
cython>=3.0.12
|
cython>=3.0.12
|
||||||
cymem>=2.0.11
|
cymem>=2.0.11
|
||||||
orjson>=3.10.15
|
orjson>=3.10.15
|
||||||
|
|||||||
3
setup.py
3
setup.py
@@ -72,7 +72,6 @@ non_apworlds: Set[str] = {
|
|||||||
"Ocarina of Time",
|
"Ocarina of Time",
|
||||||
"Overcooked! 2",
|
"Overcooked! 2",
|
||||||
"Raft",
|
"Raft",
|
||||||
"Slay the Spire",
|
|
||||||
"Sudoku",
|
"Sudoku",
|
||||||
"Super Mario 64",
|
"Super Mario 64",
|
||||||
"VVVVVV",
|
"VVVVVV",
|
||||||
@@ -154,7 +153,7 @@ if os.path.exists("X:/pw.txt"):
|
|||||||
with open("X:/pw.txt", encoding="utf-8-sig") as f:
|
with open("X:/pw.txt", encoding="utf-8-sig") as f:
|
||||||
pw = f.read()
|
pw = f.read()
|
||||||
signtool = r'signtool sign /f X:/_SITS_Zertifikat_.pfx /p "' + pw + \
|
signtool = r'signtool sign /f X:/_SITS_Zertifikat_.pfx /p "' + pw + \
|
||||||
r'" /fd sha256 /tr http://timestamp.digicert.com/ '
|
r'" /fd sha256 /td sha256 /tr http://timestamp.digicert.com/ '
|
||||||
else:
|
else:
|
||||||
signtool = None
|
signtool = None
|
||||||
|
|
||||||
|
|||||||
@@ -1,3 +1,4 @@
|
|||||||
|
from typing import Callable
|
||||||
import unittest
|
import unittest
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
|
|
||||||
@@ -34,7 +35,7 @@ def generate_entrance_pair(region: Region, name_suffix: str, group: int):
|
|||||||
|
|
||||||
|
|
||||||
def generate_disconnected_region_grid(multiworld: MultiWorld, grid_side_length: int, region_size: int = 0,
|
def generate_disconnected_region_grid(multiworld: MultiWorld, grid_side_length: int, region_size: int = 0,
|
||||||
region_type: type[Region] = Region):
|
region_creator: Callable[[str, int, MultiWorld], Region] = Region):
|
||||||
"""
|
"""
|
||||||
Generates a grid-like region structure for ER testing, where menu is connected to the top-left region, and each
|
Generates a grid-like region structure for ER testing, where menu is connected to the top-left region, and each
|
||||||
region "in vanilla" has 2 2-way exits going either down or to the right, until reaching the goal region in the
|
region "in vanilla" has 2 2-way exits going either down or to the right, until reaching the goal region in the
|
||||||
@@ -44,7 +45,7 @@ def generate_disconnected_region_grid(multiworld: MultiWorld, grid_side_length:
|
|||||||
for col in range(grid_side_length):
|
for col in range(grid_side_length):
|
||||||
index = row * grid_side_length + col
|
index = row * grid_side_length + col
|
||||||
name = f"region{index}"
|
name = f"region{index}"
|
||||||
region = region_type(name, 1, multiworld)
|
region = region_creator(name, 1, multiworld)
|
||||||
multiworld.regions.append(region)
|
multiworld.regions.append(region)
|
||||||
generate_locations(region_size, 1, region=region, tag=f"_{name}")
|
generate_locations(region_size, 1, region=region, tag=f"_{name}")
|
||||||
|
|
||||||
@@ -465,7 +466,7 @@ class TestRandomizeEntrances(unittest.TestCase):
|
|||||||
entrance_type = CustomEntrance
|
entrance_type = CustomEntrance
|
||||||
|
|
||||||
multiworld = generate_test_multiworld()
|
multiworld = generate_test_multiworld()
|
||||||
generate_disconnected_region_grid(multiworld, 5, region_type=CustomRegion)
|
generate_disconnected_region_grid(multiworld, 5, region_creator=CustomRegion)
|
||||||
|
|
||||||
self.assertRaises(EntranceRandomizationError, randomize_entrances, multiworld.worlds[1], False,
|
self.assertRaises(EntranceRandomizationError, randomize_entrances, multiworld.worlds[1], False,
|
||||||
directionally_matched_group_lookup)
|
directionally_matched_group_lookup)
|
||||||
|
|||||||
@@ -47,13 +47,39 @@ class TestIDs(unittest.TestCase):
|
|||||||
"""Test that a game doesn't have item id overlap within its own datapackage"""
|
"""Test that a game doesn't have item id overlap within its own datapackage"""
|
||||||
for gamename, world_type in AutoWorldRegister.world_types.items():
|
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||||
with self.subTest(game=gamename):
|
with self.subTest(game=gamename):
|
||||||
self.assertEqual(len(world_type.item_id_to_name), len(world_type.item_name_to_id))
|
len_item_id_to_name = len(world_type.item_id_to_name)
|
||||||
|
len_item_name_to_id = len(world_type.item_name_to_id)
|
||||||
|
|
||||||
|
if len_item_id_to_name != len_item_name_to_id:
|
||||||
|
self.assertCountEqual(
|
||||||
|
world_type.item_id_to_name.values(),
|
||||||
|
world_type.item_name_to_id.keys(),
|
||||||
|
"\nThese items have overlapping ids with other items in its own world")
|
||||||
|
self.assertCountEqual(
|
||||||
|
world_type.item_id_to_name.keys(),
|
||||||
|
world_type.item_name_to_id.values(),
|
||||||
|
"\nThese items have overlapping names with other items in its own world")
|
||||||
|
|
||||||
|
self.assertEqual(len_item_id_to_name, len_item_name_to_id)
|
||||||
|
|
||||||
def test_duplicate_location_ids(self):
|
def test_duplicate_location_ids(self):
|
||||||
"""Test that a game doesn't have location id overlap within its own datapackage"""
|
"""Test that a game doesn't have location id overlap within its own datapackage"""
|
||||||
for gamename, world_type in AutoWorldRegister.world_types.items():
|
for gamename, world_type in AutoWorldRegister.world_types.items():
|
||||||
with self.subTest(game=gamename):
|
with self.subTest(game=gamename):
|
||||||
self.assertEqual(len(world_type.location_id_to_name), len(world_type.location_name_to_id))
|
len_location_id_to_name = len(world_type.location_id_to_name)
|
||||||
|
len_location_name_to_id = len(world_type.location_name_to_id)
|
||||||
|
|
||||||
|
if len_location_id_to_name != len_location_name_to_id:
|
||||||
|
self.assertCountEqual(
|
||||||
|
world_type.location_id_to_name.values(),
|
||||||
|
world_type.location_name_to_id.keys(),
|
||||||
|
"\nThese locations have overlapping ids with other locations in its own world")
|
||||||
|
self.assertCountEqual(
|
||||||
|
world_type.location_id_to_name.keys(),
|
||||||
|
world_type.location_name_to_id.values(),
|
||||||
|
"\nThese locations have overlapping names with other locations in its own world")
|
||||||
|
|
||||||
|
self.assertEqual(len_location_id_to_name, len_location_name_to_id)
|
||||||
|
|
||||||
def test_postgen_datapackage(self):
|
def test_postgen_datapackage(self):
|
||||||
"""Generates a solo multiworld and checks that the datapackage is still valid"""
|
"""Generates a solo multiworld and checks that the datapackage is still valid"""
|
||||||
|
|||||||
@@ -1,7 +1,11 @@
|
|||||||
import unittest
|
import unittest
|
||||||
|
from argparse import Namespace
|
||||||
|
from typing import Type
|
||||||
|
|
||||||
from BaseClasses import CollectionState
|
from BaseClasses import CollectionState, MultiWorld
|
||||||
from worlds.AutoWorld import AutoWorldRegister, call_all
|
from Fill import distribute_items_restrictive
|
||||||
|
from Options import ItemLinks
|
||||||
|
from worlds.AutoWorld import AutoWorldRegister, World, call_all
|
||||||
from . import setup_solo_multiworld
|
from . import setup_solo_multiworld
|
||||||
|
|
||||||
|
|
||||||
@@ -84,6 +88,47 @@ class TestBase(unittest.TestCase):
|
|||||||
for item in multiworld.itempool:
|
for item in multiworld.itempool:
|
||||||
self.assertIn(item.name, world_type.item_name_to_id)
|
self.assertIn(item.name, world_type.item_name_to_id)
|
||||||
|
|
||||||
|
def test_item_links(self) -> None:
|
||||||
|
"""
|
||||||
|
Tests item link creation by creating a multiworld of 2 worlds for every game and linking their items together.
|
||||||
|
"""
|
||||||
|
def setup_link_multiworld(world: Type[World], link_replace: bool) -> None:
|
||||||
|
multiworld = MultiWorld(2)
|
||||||
|
multiworld.game = {1: world.game, 2: world.game}
|
||||||
|
multiworld.player_name = {1: "Linker 1", 2: "Linker 2"}
|
||||||
|
multiworld.set_seed()
|
||||||
|
item_link_group = [{
|
||||||
|
"name": "ItemLinkTest",
|
||||||
|
"item_pool": ["Everything"],
|
||||||
|
"link_replacement": link_replace,
|
||||||
|
"replacement_item": None,
|
||||||
|
}]
|
||||||
|
args = Namespace()
|
||||||
|
for name, option in world.options_dataclass.type_hints.items():
|
||||||
|
setattr(args, name, {1: option.from_any(option.default), 2: option.from_any(option.default)})
|
||||||
|
setattr(args, "item_links",
|
||||||
|
{1: ItemLinks.from_any(item_link_group), 2: ItemLinks.from_any(item_link_group)})
|
||||||
|
multiworld.set_options(args)
|
||||||
|
multiworld.set_item_links()
|
||||||
|
# groups get added to state during its constructor so this has to be after item links are set
|
||||||
|
multiworld.state = CollectionState(multiworld)
|
||||||
|
gen_steps = ("generate_early", "create_regions", "create_items", "set_rules", "connect_entrances", "generate_basic")
|
||||||
|
for step in gen_steps:
|
||||||
|
call_all(multiworld, step)
|
||||||
|
# link the items together and attempt to fill
|
||||||
|
multiworld.link_items()
|
||||||
|
multiworld._all_state = None
|
||||||
|
call_all(multiworld, "pre_fill")
|
||||||
|
distribute_items_restrictive(multiworld)
|
||||||
|
call_all(multiworld, "post_fill")
|
||||||
|
self.assertTrue(multiworld.can_beat_game(CollectionState(multiworld)), f"seed = {multiworld.seed}")
|
||||||
|
|
||||||
|
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||||
|
with self.subTest("Can generate with link replacement", game=game_name):
|
||||||
|
setup_link_multiworld(world_type, True)
|
||||||
|
with self.subTest("Can generate without link replacement", game=game_name):
|
||||||
|
setup_link_multiworld(world_type, False)
|
||||||
|
|
||||||
def test_itempool_not_modified(self):
|
def test_itempool_not_modified(self):
|
||||||
"""Test that worlds don't modify the itempool after `create_items`"""
|
"""Test that worlds don't modify the itempool after `create_items`"""
|
||||||
gen_steps = ("generate_early", "create_regions", "create_items")
|
gen_steps = ("generate_early", "create_regions", "create_items")
|
||||||
|
|||||||
@@ -80,8 +80,8 @@ class Client:
|
|||||||
"version": {
|
"version": {
|
||||||
"class": "Version",
|
"class": "Version",
|
||||||
"major": 0,
|
"major": 0,
|
||||||
"minor": 4,
|
"minor": 6,
|
||||||
"build": 6,
|
"build": 0,
|
||||||
},
|
},
|
||||||
"items_handling": 0,
|
"items_handling": 0,
|
||||||
"tags": [],
|
"tags": [],
|
||||||
|
|||||||
@@ -47,17 +47,6 @@ class TestCommonContext(unittest.IsolatedAsyncioTestCase):
|
|||||||
assert "Archipelago" in self.ctx.item_names, "Archipelago item names entry does not exist"
|
assert "Archipelago" in self.ctx.item_names, "Archipelago item names entry does not exist"
|
||||||
assert "Archipelago" in self.ctx.location_names, "Archipelago location names entry does not exist"
|
assert "Archipelago" in self.ctx.location_names, "Archipelago location names entry does not exist"
|
||||||
|
|
||||||
async def test_implicit_name_lookups(self):
|
|
||||||
# Items
|
|
||||||
assert self.ctx.item_names[2**54 + 1] == "Test Item 1 - Safe"
|
|
||||||
assert self.ctx.item_names[2**54 + 3] == f"Unknown item (ID: {2**54+3})"
|
|
||||||
assert self.ctx.item_names[-1] == "Nothing"
|
|
||||||
|
|
||||||
# Locations
|
|
||||||
assert self.ctx.location_names[2**54 + 1] == "Test Location 1 - Safe"
|
|
||||||
assert self.ctx.location_names[2**54 + 3] == f"Unknown location (ID: {2**54+3})"
|
|
||||||
assert self.ctx.location_names[-1] == "Cheat Console"
|
|
||||||
|
|
||||||
async def test_explicit_name_lookups(self):
|
async def test_explicit_name_lookups(self):
|
||||||
# Items
|
# Items
|
||||||
assert self.ctx.item_names["__TestGame1"][2**54+1] == "Test Item 1 - Safe"
|
assert self.ctx.item_names["__TestGame1"][2**54+1] == "Test Item 1 - Safe"
|
||||||
|
|||||||
@@ -35,6 +35,19 @@ class TestCacheSelf1(unittest.TestCase):
|
|||||||
self.assertFalse(o1.func(1) is o1.func(2))
|
self.assertFalse(o1.func(1) is o1.func(2))
|
||||||
self.assertFalse(o1.func(1) is o2.func(1))
|
self.assertFalse(o1.func(1) is o2.func(1))
|
||||||
|
|
||||||
|
def test_cache_default(self) -> None:
|
||||||
|
class Cls:
|
||||||
|
@cache_self1
|
||||||
|
def func(self, _: Any = 1) -> object:
|
||||||
|
return object()
|
||||||
|
|
||||||
|
o1 = Cls()
|
||||||
|
o2 = Cls()
|
||||||
|
self.assertIs(o1.func(), o1.func())
|
||||||
|
self.assertIs(o1.func(1), o1.func())
|
||||||
|
self.assertIsNot(o1.func(2), o1.func())
|
||||||
|
self.assertIsNot(o1.func(), o2.func())
|
||||||
|
|
||||||
def test_gc(self) -> None:
|
def test_gc(self) -> None:
|
||||||
# verify that we don't keep a global reference
|
# verify that we don't keep a global reference
|
||||||
import gc
|
import gc
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ import unittest
|
|||||||
|
|
||||||
from BaseClasses import PlandoOptions
|
from BaseClasses import PlandoOptions
|
||||||
from worlds import AutoWorldRegister
|
from worlds import AutoWorldRegister
|
||||||
from Options import ItemDict, NamedRange, NumericOption, OptionList, OptionSet
|
from Options import OptionCounter, NamedRange, NumericOption, OptionList, OptionSet
|
||||||
|
|
||||||
|
|
||||||
class TestOptionPresets(unittest.TestCase):
|
class TestOptionPresets(unittest.TestCase):
|
||||||
@@ -19,7 +19,7 @@ class TestOptionPresets(unittest.TestCase):
|
|||||||
# pass in all plando options in case a preset wants to require certain plando options
|
# pass in all plando options in case a preset wants to require certain plando options
|
||||||
# for some reason
|
# for some reason
|
||||||
option.verify(world_type, "Test Player", PlandoOptions(sum(PlandoOptions)))
|
option.verify(world_type, "Test Player", PlandoOptions(sum(PlandoOptions)))
|
||||||
supported_types = [NumericOption, OptionSet, OptionList, ItemDict]
|
supported_types = [NumericOption, OptionSet, OptionList, OptionCounter]
|
||||||
if not any([issubclass(option.__class__, t) for t in supported_types]):
|
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}' "
|
self.fail(f"'{option_name}' in preset '{preset_name}' for game '{game_name}' "
|
||||||
f"is not a supported type for webhost. "
|
f"is not a supported type for webhost. "
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ def load_tests(loader, standard_tests, pattern):
|
|||||||
all_tests = [
|
all_tests = [
|
||||||
test_case for folder in folders if os.path.exists(folder)
|
test_case for folder in folders if os.path.exists(folder)
|
||||||
for test_collection in loader.discover(folder, top_level_dir=file_path)
|
for test_collection in loader.discover(folder, top_level_dir=file_path)
|
||||||
for test_suite in test_collection
|
for test_suite in test_collection if isinstance(test_suite, unittest.suite.TestSuite)
|
||||||
for test_case in test_suite
|
for test_case in test_suite
|
||||||
]
|
]
|
||||||
|
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ from typing import (Any, Callable, ClassVar, Dict, FrozenSet, Iterable, List, Ma
|
|||||||
|
|
||||||
from Options import item_and_loc_options, ItemsAccessibility, OptionGroup, PerGameCommonOptions
|
from Options import item_and_loc_options, ItemsAccessibility, OptionGroup, PerGameCommonOptions
|
||||||
from BaseClasses import CollectionState
|
from BaseClasses import CollectionState
|
||||||
|
from Utils import deprecate
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from BaseClasses import MultiWorld, Item, Location, Tutorial, Region, Entrance
|
from BaseClasses import MultiWorld, Item, Location, Tutorial, Region, Entrance
|
||||||
@@ -75,19 +76,20 @@ class AutoWorldRegister(type):
|
|||||||
# TODO - remove this once all worlds use options dataclasses
|
# TODO - remove this once all worlds use options dataclasses
|
||||||
if "options_dataclass" not in dct and "option_definitions" in dct:
|
if "options_dataclass" not in dct and "option_definitions" in dct:
|
||||||
# TODO - switch to deprecate after a version
|
# TODO - switch to deprecate after a version
|
||||||
if __debug__:
|
deprecate(f"{name} Assigned options through option_definitions which is now deprecated. "
|
||||||
logging.warning(f"{name} Assigned options through option_definitions which is now deprecated. "
|
"Please use options_dataclass instead.")
|
||||||
"Please use options_dataclass instead.")
|
|
||||||
dct["options_dataclass"] = make_dataclass(f"{name}Options", dct["option_definitions"].items(),
|
dct["options_dataclass"] = make_dataclass(f"{name}Options", dct["option_definitions"].items(),
|
||||||
bases=(PerGameCommonOptions,))
|
bases=(PerGameCommonOptions,))
|
||||||
|
|
||||||
# construct class
|
# construct class
|
||||||
new_class = super().__new__(mcs, name, bases, dct)
|
new_class = super().__new__(mcs, name, bases, dct)
|
||||||
|
new_class.__file__ = sys.modules[new_class.__module__].__file__
|
||||||
if "game" in dct:
|
if "game" in dct:
|
||||||
if dct["game"] in AutoWorldRegister.world_types:
|
if dct["game"] in AutoWorldRegister.world_types:
|
||||||
raise RuntimeError(f"""Game {dct["game"]} already registered.""")
|
raise RuntimeError(f"""Game {dct["game"]} already registered in
|
||||||
|
{AutoWorldRegister.world_types[dct["game"]].__file__} when attempting to register from
|
||||||
|
{new_class.__file__}.""")
|
||||||
AutoWorldRegister.world_types[dct["game"]] = new_class
|
AutoWorldRegister.world_types[dct["game"]] = new_class
|
||||||
new_class.__file__ = sys.modules[new_class.__module__].__file__
|
|
||||||
if ".apworld" in new_class.__file__:
|
if ".apworld" in new_class.__file__:
|
||||||
new_class.zip_path = pathlib.Path(new_class.__file__).parents[1]
|
new_class.zip_path = pathlib.Path(new_class.__file__).parents[1]
|
||||||
if "settings_key" not in dct:
|
if "settings_key" not in dct:
|
||||||
@@ -483,7 +485,7 @@ class World(metaclass=AutoWorldRegister):
|
|||||||
def get_filler_item_name(self) -> str:
|
def get_filler_item_name(self) -> str:
|
||||||
"""Called when the item pool needs to be filled with additional items to match location count."""
|
"""Called when the item pool needs to be filled with additional items to match location count."""
|
||||||
logging.warning(f"World {self} is generating a filler item without custom filler pool.")
|
logging.warning(f"World {self} is generating a filler item without custom filler pool.")
|
||||||
return self.multiworld.random.choice(tuple(self.item_name_to_id.keys()))
|
return self.random.choice(tuple(self.item_name_to_id.keys()))
|
||||||
|
|
||||||
@classmethod
|
@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:
|
||||||
|
|||||||
@@ -102,7 +102,7 @@ def KholdstareDefeatRule(state, player: int) -> bool:
|
|||||||
state.has('Fire Rod', player) or
|
state.has('Fire Rod', player) or
|
||||||
(
|
(
|
||||||
state.has('Bombos', player) and
|
state.has('Bombos', player) and
|
||||||
(has_sword(state, player) or state.multiworld.swordless[player])
|
(has_sword(state, player) or state.multiworld.worlds[player].options.swordless)
|
||||||
)
|
)
|
||||||
) and
|
) and
|
||||||
(
|
(
|
||||||
@@ -111,7 +111,7 @@ def KholdstareDefeatRule(state, player: int) -> bool:
|
|||||||
(
|
(
|
||||||
state.has('Fire Rod', player) and
|
state.has('Fire Rod', player) and
|
||||||
state.has('Bombos', player) and
|
state.has('Bombos', player) and
|
||||||
state.multiworld.swordless[player] and
|
state.multiworld.worlds[player].options.swordless and
|
||||||
can_extend_magic(state, player, 16)
|
can_extend_magic(state, player, 16)
|
||||||
)
|
)
|
||||||
)
|
)
|
||||||
@@ -137,7 +137,7 @@ def AgahnimDefeatRule(state, player: int) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def GanonDefeatRule(state, player: int) -> bool:
|
def GanonDefeatRule(state, player: int) -> bool:
|
||||||
if state.multiworld.swordless[player]:
|
if state.multiworld.worlds[player].options.swordless:
|
||||||
return state.has('Hammer', player) and \
|
return state.has('Hammer', player) and \
|
||||||
has_fire_source(state, player) and \
|
has_fire_source(state, player) and \
|
||||||
state.has('Silver Bow', player) and \
|
state.has('Silver Bow', player) and \
|
||||||
@@ -146,7 +146,7 @@ def GanonDefeatRule(state, player: int) -> bool:
|
|||||||
can_hurt = has_beam_sword(state, player)
|
can_hurt = has_beam_sword(state, player)
|
||||||
common = can_hurt and has_fire_source(state, player)
|
common = can_hurt and has_fire_source(state, player)
|
||||||
# silverless ganon may be needed in anything higher than no glitches
|
# silverless ganon may be needed in anything higher than no glitches
|
||||||
if state.multiworld.glitches_required[player] != 'no_glitches':
|
if state.multiworld.worlds[player].options.glitches_required != 'no_glitches':
|
||||||
# need to light torch a sufficient amount of times
|
# need to light torch a sufficient amount of times
|
||||||
return common and (state.has('Tempered Sword', player) or state.has('Golden Sword', player) or (
|
return common and (state.has('Tempered Sword', player) or state.has('Golden Sword', player) or (
|
||||||
state.has('Silver Bow', player) and can_shoot_arrows(state, player)) or
|
state.has('Silver Bow', player) and can_shoot_arrows(state, player)) or
|
||||||
@@ -248,7 +248,7 @@ for location in boss_location_table:
|
|||||||
|
|
||||||
def place_boss(world: "ALTTPWorld", boss: str, location: str, level: Optional[str]) -> None:
|
def place_boss(world: "ALTTPWorld", boss: str, location: str, level: Optional[str]) -> None:
|
||||||
player = world.player
|
player = world.player
|
||||||
if location == 'Ganons Tower' and world.multiworld.mode[player] == 'inverted':
|
if location == 'Ganons Tower' and world.options.mode == 'inverted':
|
||||||
location = 'Inverted Ganons Tower'
|
location = 'Inverted Ganons Tower'
|
||||||
logging.debug('Placing boss %s at %s', boss, location + (' (' + level + ')' if level else ''))
|
logging.debug('Placing boss %s at %s', boss, location + (' (' + level + ')' if level else ''))
|
||||||
world.dungeons[location].bosses[level] = BossFactory(boss, player)
|
world.dungeons[location].bosses[level] = BossFactory(boss, player)
|
||||||
@@ -260,9 +260,8 @@ def format_boss_location(location_name: str, level: str) -> str:
|
|||||||
|
|
||||||
def place_bosses(world: "ALTTPWorld") -> None:
|
def place_bosses(world: "ALTTPWorld") -> None:
|
||||||
multiworld = world.multiworld
|
multiworld = world.multiworld
|
||||||
player = world.player
|
|
||||||
# will either be an int or a lower case string with ';' between options
|
# will either be an int or a lower case string with ';' between options
|
||||||
boss_shuffle: Union[str, int] = multiworld.boss_shuffle[player].value
|
boss_shuffle: Union[str, int] = world.options.boss_shuffle.value
|
||||||
already_placed_bosses: List[str] = []
|
already_placed_bosses: List[str] = []
|
||||||
remaining_locations: List[Tuple[str, str]] = []
|
remaining_locations: List[Tuple[str, str]] = []
|
||||||
# handle plando
|
# handle plando
|
||||||
|
|||||||
@@ -66,7 +66,7 @@ def create_dungeons(world: "ALTTPWorld"):
|
|||||||
|
|
||||||
def make_dungeon(name, default_boss, dungeon_regions, big_key, small_keys, dungeon_items):
|
def make_dungeon(name, default_boss, dungeon_regions, big_key, small_keys, dungeon_items):
|
||||||
dungeon = Dungeon(name, dungeon_regions, big_key,
|
dungeon = Dungeon(name, dungeon_regions, big_key,
|
||||||
[] if multiworld.small_key_shuffle[player] == small_key_shuffle.option_universal else small_keys,
|
[] if multiworld.worlds[player].options.small_key_shuffle == small_key_shuffle.option_universal else small_keys,
|
||||||
dungeon_items, player)
|
dungeon_items, player)
|
||||||
for item in dungeon.all_items:
|
for item in dungeon.all_items:
|
||||||
item.dungeon = dungeon
|
item.dungeon = dungeon
|
||||||
@@ -143,7 +143,7 @@ def create_dungeons(world: "ALTTPWorld"):
|
|||||||
item_factory(['Small Key (Turtle Rock)'] * 6, world),
|
item_factory(['Small Key (Turtle Rock)'] * 6, world),
|
||||||
item_factory(['Map (Turtle Rock)', 'Compass (Turtle Rock)'], world))
|
item_factory(['Map (Turtle Rock)', 'Compass (Turtle Rock)'], world))
|
||||||
|
|
||||||
if multiworld.mode[player] != 'inverted':
|
if multiworld.worlds[player].options.mode != 'inverted':
|
||||||
AT = make_dungeon('Agahnims Tower', 'Agahnim', ['Agahnims Tower', 'Agahnim 1'], None,
|
AT = make_dungeon('Agahnims Tower', 'Agahnim', ['Agahnims Tower', 'Agahnim 1'], None,
|
||||||
item_factory(['Small Key (Agahnims Tower)'] * 4, world), [])
|
item_factory(['Small Key (Agahnims Tower)'] * 4, world), [])
|
||||||
GT = make_dungeon('Ganons Tower', 'Agahnim2',
|
GT = make_dungeon('Ganons Tower', 'Agahnim2',
|
||||||
|
|||||||
@@ -23,17 +23,17 @@ def link_entrances(world, player):
|
|||||||
connect_simple(world, exitname, regionname, player)
|
connect_simple(world, exitname, regionname, player)
|
||||||
|
|
||||||
# if we do not shuffle, set default connections
|
# if we do not shuffle, set default connections
|
||||||
if world.entrance_shuffle[player] == 'vanilla':
|
if world.worlds[player].options.entrance_shuffle == 'vanilla':
|
||||||
for exitname, regionname in default_connections:
|
for exitname, regionname in default_connections:
|
||||||
connect_simple(world, exitname, regionname, player)
|
connect_simple(world, exitname, regionname, player)
|
||||||
for exitname, regionname in default_dungeon_connections:
|
for exitname, regionname in default_dungeon_connections:
|
||||||
connect_simple(world, exitname, regionname, player)
|
connect_simple(world, exitname, regionname, player)
|
||||||
elif world.entrance_shuffle[player] == 'dungeons_simple':
|
elif world.worlds[player].options.entrance_shuffle == 'dungeons_simple':
|
||||||
for exitname, regionname in default_connections:
|
for exitname, regionname in default_connections:
|
||||||
connect_simple(world, exitname, regionname, player)
|
connect_simple(world, exitname, regionname, player)
|
||||||
|
|
||||||
simple_shuffle_dungeons(world, player)
|
simple_shuffle_dungeons(world, player)
|
||||||
elif world.entrance_shuffle[player] == 'dungeons_full':
|
elif world.worlds[player].options.entrance_shuffle == 'dungeons_full':
|
||||||
for exitname, regionname in default_connections:
|
for exitname, regionname in default_connections:
|
||||||
connect_simple(world, exitname, regionname, player)
|
connect_simple(world, exitname, regionname, player)
|
||||||
|
|
||||||
@@ -43,7 +43,7 @@ def link_entrances(world, player):
|
|||||||
lw_entrances = list(LW_Dungeon_Entrances)
|
lw_entrances = list(LW_Dungeon_Entrances)
|
||||||
dw_entrances = list(DW_Dungeon_Entrances)
|
dw_entrances = list(DW_Dungeon_Entrances)
|
||||||
|
|
||||||
if world.mode[player] == 'standard':
|
if world.worlds[player].options.mode == 'standard':
|
||||||
# must connect front of hyrule castle to do escape
|
# must connect front of hyrule castle to do escape
|
||||||
connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
|
connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
|
||||||
else:
|
else:
|
||||||
@@ -56,7 +56,7 @@ def link_entrances(world, player):
|
|||||||
dw_entrances.append('Ganons Tower')
|
dw_entrances.append('Ganons Tower')
|
||||||
dungeon_exits.append('Ganons Tower Exit')
|
dungeon_exits.append('Ganons Tower Exit')
|
||||||
|
|
||||||
if world.mode[player] == 'standard':
|
if world.worlds[player].options.mode == 'standard':
|
||||||
# rest of hyrule castle must be in light world, so it has to be the one connected to east exit of desert
|
# rest of hyrule castle must be in light world, so it has to be the one connected to east exit of desert
|
||||||
hyrule_castle_exits = [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')]
|
hyrule_castle_exits = [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')]
|
||||||
connect_mandatory_exits(world, lw_entrances, hyrule_castle_exits, list(LW_Dungeon_Entrances_Must_Exit), player)
|
connect_mandatory_exits(world, lw_entrances, hyrule_castle_exits, list(LW_Dungeon_Entrances_Must_Exit), player)
|
||||||
@@ -65,9 +65,9 @@ def link_entrances(world, player):
|
|||||||
connect_mandatory_exits(world, lw_entrances, dungeon_exits, list(LW_Dungeon_Entrances_Must_Exit), player)
|
connect_mandatory_exits(world, lw_entrances, dungeon_exits, list(LW_Dungeon_Entrances_Must_Exit), player)
|
||||||
connect_mandatory_exits(world, dw_entrances, dungeon_exits, list(DW_Dungeon_Entrances_Must_Exit), player)
|
connect_mandatory_exits(world, dw_entrances, dungeon_exits, list(DW_Dungeon_Entrances_Must_Exit), player)
|
||||||
connect_caves(world, lw_entrances, dw_entrances, dungeon_exits, player)
|
connect_caves(world, lw_entrances, dw_entrances, dungeon_exits, player)
|
||||||
elif world.entrance_shuffle[player] == 'dungeons_crossed':
|
elif world.worlds[player].options.entrance_shuffle == 'dungeons_crossed':
|
||||||
crossed_shuffle_dungeons(world, player)
|
crossed_shuffle_dungeons(world, player)
|
||||||
elif world.entrance_shuffle[player] == 'simple':
|
elif world.worlds[player].options.entrance_shuffle == 'simple':
|
||||||
simple_shuffle_dungeons(world, player)
|
simple_shuffle_dungeons(world, player)
|
||||||
|
|
||||||
old_man_entrances = list(Old_Man_Entrances)
|
old_man_entrances = list(Old_Man_Entrances)
|
||||||
@@ -138,7 +138,7 @@ def link_entrances(world, player):
|
|||||||
|
|
||||||
# place remaining doors
|
# place remaining doors
|
||||||
connect_doors(world, single_doors, door_targets, player)
|
connect_doors(world, single_doors, door_targets, player)
|
||||||
elif world.entrance_shuffle[player] == 'restricted':
|
elif world.worlds[player].options.entrance_shuffle == 'restricted':
|
||||||
simple_shuffle_dungeons(world, player)
|
simple_shuffle_dungeons(world, player)
|
||||||
|
|
||||||
lw_entrances = list(LW_Entrances + LW_Single_Cave_Doors + Old_Man_Entrances)
|
lw_entrances = list(LW_Entrances + LW_Single_Cave_Doors + Old_Man_Entrances)
|
||||||
@@ -210,7 +210,7 @@ def link_entrances(world, player):
|
|||||||
# place remaining doors
|
# place remaining doors
|
||||||
connect_doors(world, doors, door_targets, player)
|
connect_doors(world, doors, door_targets, player)
|
||||||
|
|
||||||
elif world.entrance_shuffle[player] == 'full':
|
elif world.worlds[player].options.entrance_shuffle == 'full':
|
||||||
skull_woods_shuffle(world, player)
|
skull_woods_shuffle(world, player)
|
||||||
|
|
||||||
lw_entrances = list(LW_Entrances + LW_Dungeon_Entrances + LW_Single_Cave_Doors + Old_Man_Entrances)
|
lw_entrances = list(LW_Entrances + LW_Dungeon_Entrances + LW_Single_Cave_Doors + Old_Man_Entrances)
|
||||||
@@ -227,7 +227,7 @@ def link_entrances(world, player):
|
|||||||
# tavern back door cannot be shuffled yet
|
# tavern back door cannot be shuffled yet
|
||||||
connect_doors(world, ['Tavern North'], ['Tavern'], player)
|
connect_doors(world, ['Tavern North'], ['Tavern'], player)
|
||||||
|
|
||||||
if world.mode[player] == 'standard':
|
if world.worlds[player].options.mode == 'standard':
|
||||||
# must connect front of hyrule castle to do escape
|
# must connect front of hyrule castle to do escape
|
||||||
connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
|
connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
|
||||||
else:
|
else:
|
||||||
@@ -264,7 +264,7 @@ def link_entrances(world, player):
|
|||||||
pass
|
pass
|
||||||
else: #if the cave wasn't placed we get here
|
else: #if the cave wasn't placed we get here
|
||||||
connect_caves(world, lw_entrances, [], old_man_house, player)
|
connect_caves(world, lw_entrances, [], old_man_house, player)
|
||||||
if world.mode[player] == 'standard':
|
if world.worlds[player].options.mode == 'standard':
|
||||||
# rest of hyrule castle must be in light world
|
# rest of hyrule castle must be in light world
|
||||||
connect_caves(world, lw_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player)
|
connect_caves(world, lw_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player)
|
||||||
|
|
||||||
@@ -316,7 +316,7 @@ def link_entrances(world, player):
|
|||||||
|
|
||||||
# place remaining doors
|
# place remaining doors
|
||||||
connect_doors(world, doors, door_targets, player)
|
connect_doors(world, doors, door_targets, player)
|
||||||
elif world.entrance_shuffle[player] == 'crossed':
|
elif world.worlds[player].options.entrance_shuffle == 'crossed':
|
||||||
skull_woods_shuffle(world, player)
|
skull_woods_shuffle(world, player)
|
||||||
|
|
||||||
entrances = list(LW_Entrances + LW_Dungeon_Entrances + LW_Single_Cave_Doors + Old_Man_Entrances + DW_Entrances + DW_Dungeon_Entrances + DW_Single_Cave_Doors)
|
entrances = list(LW_Entrances + LW_Dungeon_Entrances + LW_Single_Cave_Doors + Old_Man_Entrances + DW_Entrances + DW_Dungeon_Entrances + DW_Single_Cave_Doors)
|
||||||
@@ -331,7 +331,7 @@ def link_entrances(world, player):
|
|||||||
# tavern back door cannot be shuffled yet
|
# tavern back door cannot be shuffled yet
|
||||||
connect_doors(world, ['Tavern North'], ['Tavern'], player)
|
connect_doors(world, ['Tavern North'], ['Tavern'], player)
|
||||||
|
|
||||||
if world.mode[player] == 'standard':
|
if world.worlds[player].options.mode == 'standard':
|
||||||
# must connect front of hyrule castle to do escape
|
# must connect front of hyrule castle to do escape
|
||||||
connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
|
connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
|
||||||
else:
|
else:
|
||||||
@@ -348,7 +348,7 @@ def link_entrances(world, player):
|
|||||||
#place must-exit caves
|
#place must-exit caves
|
||||||
connect_mandatory_exits(world, entrances, caves, must_exits, player)
|
connect_mandatory_exits(world, entrances, caves, must_exits, player)
|
||||||
|
|
||||||
if world.mode[player] == 'standard':
|
if world.worlds[player].options.mode == 'standard':
|
||||||
# rest of hyrule castle must be dealt with
|
# rest of hyrule castle must be dealt with
|
||||||
connect_caves(world, entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player)
|
connect_caves(world, entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player)
|
||||||
|
|
||||||
@@ -394,7 +394,7 @@ def link_entrances(world, player):
|
|||||||
# place remaining doors
|
# place remaining doors
|
||||||
connect_doors(world, entrances, door_targets, player)
|
connect_doors(world, entrances, door_targets, player)
|
||||||
|
|
||||||
elif world.entrance_shuffle[player] == 'insanity':
|
elif world.worlds[player].options.entrance_shuffle == 'insanity':
|
||||||
# beware ye who enter here
|
# beware ye who enter here
|
||||||
|
|
||||||
entrances = LW_Entrances + LW_Dungeon_Entrances + DW_Entrances + DW_Dungeon_Entrances + Old_Man_Entrances + ['Skull Woods Second Section Door (East)', 'Skull Woods First Section Door', 'Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave']
|
entrances = LW_Entrances + LW_Dungeon_Entrances + DW_Entrances + DW_Dungeon_Entrances + Old_Man_Entrances + ['Skull Woods Second Section Door (East)', 'Skull Woods First Section Door', 'Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave']
|
||||||
@@ -431,7 +431,7 @@ def link_entrances(world, player):
|
|||||||
# tavern back door cannot be shuffled yet
|
# tavern back door cannot be shuffled yet
|
||||||
connect_doors(world, ['Tavern North'], ['Tavern'], player)
|
connect_doors(world, ['Tavern North'], ['Tavern'], player)
|
||||||
|
|
||||||
if world.mode[player] == 'standard':
|
if world.worlds[player].options.mode == 'standard':
|
||||||
# cannot move uncle cave
|
# cannot move uncle cave
|
||||||
connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player)
|
connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player)
|
||||||
connect_exit(world, 'Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance Stairs', player)
|
connect_exit(world, 'Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance Stairs', player)
|
||||||
@@ -464,7 +464,7 @@ def link_entrances(world, player):
|
|||||||
connect_entrance(world, hole, hole_targets.pop(), player)
|
connect_entrance(world, hole, hole_targets.pop(), player)
|
||||||
|
|
||||||
# hyrule castle handling
|
# hyrule castle handling
|
||||||
if world.mode[player] == 'standard':
|
if world.worlds[player].options.mode == 'standard':
|
||||||
# must connect front of hyrule castle to do escape
|
# must connect front of hyrule castle to do escape
|
||||||
connect_entrance(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
|
connect_entrance(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
|
||||||
connect_exit(world, 'Hyrule Castle Exit (South)', 'Hyrule Castle Entrance (South)', player)
|
connect_exit(world, 'Hyrule Castle Exit (South)', 'Hyrule Castle Entrance (South)', player)
|
||||||
@@ -544,12 +544,12 @@ def link_entrances(world, player):
|
|||||||
|
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError(
|
raise NotImplementedError(
|
||||||
f'{world.entrance_shuffle[player]} Shuffling not supported yet. Player {world.get_player_name(player)}')
|
f'{world.worlds[player].options.entrance_shuffle} Shuffling not supported yet. Player {world.get_player_name(player)}')
|
||||||
|
|
||||||
if world.glitches_required[player] in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']:
|
if world.worlds[player].options.glitches_required in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']:
|
||||||
overworld_glitch_connections(world, player)
|
overworld_glitch_connections(world, player)
|
||||||
# mandatory hybrid major glitches connections
|
# mandatory hybrid major glitches connections
|
||||||
if world.glitches_required[player] in ['hybrid_major_glitches', 'no_logic']:
|
if world.worlds[player].options.glitches_required in ['hybrid_major_glitches', 'no_logic']:
|
||||||
underworld_glitch_connections(world, player)
|
underworld_glitch_connections(world, player)
|
||||||
|
|
||||||
# check for swamp palace fix
|
# check for swamp palace fix
|
||||||
@@ -584,17 +584,17 @@ def link_inverted_entrances(world, player):
|
|||||||
connect_simple(world, exitname, regionname, player)
|
connect_simple(world, exitname, regionname, player)
|
||||||
|
|
||||||
# if we do not shuffle, set default connections
|
# if we do not shuffle, set default connections
|
||||||
if world.entrance_shuffle[player] == 'vanilla':
|
if world.worlds[player].options.entrance_shuffle == 'vanilla':
|
||||||
for exitname, regionname in inverted_default_connections:
|
for exitname, regionname in inverted_default_connections:
|
||||||
connect_simple(world, exitname, regionname, player)
|
connect_simple(world, exitname, regionname, player)
|
||||||
for exitname, regionname in inverted_default_dungeon_connections:
|
for exitname, regionname in inverted_default_dungeon_connections:
|
||||||
connect_simple(world, exitname, regionname, player)
|
connect_simple(world, exitname, regionname, player)
|
||||||
elif world.entrance_shuffle[player] == 'dungeons_simple':
|
elif world.worlds[player].options.entrance_shuffle == 'dungeons_simple':
|
||||||
for exitname, regionname in inverted_default_connections:
|
for exitname, regionname in inverted_default_connections:
|
||||||
connect_simple(world, exitname, regionname, player)
|
connect_simple(world, exitname, regionname, player)
|
||||||
|
|
||||||
simple_shuffle_dungeons(world, player)
|
simple_shuffle_dungeons(world, player)
|
||||||
elif world.entrance_shuffle[player] == 'dungeons_full':
|
elif world.worlds[player].options.entrance_shuffle == 'dungeons_full':
|
||||||
for exitname, regionname in inverted_default_connections:
|
for exitname, regionname in inverted_default_connections:
|
||||||
connect_simple(world, exitname, regionname, player)
|
connect_simple(world, exitname, regionname, player)
|
||||||
|
|
||||||
@@ -649,9 +649,9 @@ def link_inverted_entrances(world, player):
|
|||||||
connect_mandatory_exits(world, lw_entrances, dungeon_exits, lw_dungeon_entrances_must_exit, player)
|
connect_mandatory_exits(world, lw_entrances, dungeon_exits, lw_dungeon_entrances_must_exit, player)
|
||||||
|
|
||||||
connect_caves(world, lw_entrances, dw_entrances, dungeon_exits, player)
|
connect_caves(world, lw_entrances, dw_entrances, dungeon_exits, player)
|
||||||
elif world.entrance_shuffle[player] == 'dungeons_crossed':
|
elif world.worlds[player].options.entrance_shuffle == 'dungeons_crossed':
|
||||||
inverted_crossed_shuffle_dungeons(world, player)
|
inverted_crossed_shuffle_dungeons(world, player)
|
||||||
elif world.entrance_shuffle[player] == 'simple':
|
elif world.worlds[player].options.entrance_shuffle == 'simple':
|
||||||
simple_shuffle_dungeons(world, player)
|
simple_shuffle_dungeons(world, player)
|
||||||
|
|
||||||
old_man_entrances = list(Inverted_Old_Man_Entrances)
|
old_man_entrances = list(Inverted_Old_Man_Entrances)
|
||||||
@@ -748,7 +748,7 @@ def link_inverted_entrances(world, player):
|
|||||||
# place remaining doors
|
# place remaining doors
|
||||||
connect_doors(world, single_doors, door_targets, player)
|
connect_doors(world, single_doors, door_targets, player)
|
||||||
|
|
||||||
elif world.entrance_shuffle[player] == 'restricted':
|
elif world.worlds[player].options.entrance_shuffle == 'restricted':
|
||||||
simple_shuffle_dungeons(world, player)
|
simple_shuffle_dungeons(world, player)
|
||||||
|
|
||||||
lw_entrances = list(Inverted_LW_Entrances + Inverted_LW_Single_Cave_Doors)
|
lw_entrances = list(Inverted_LW_Entrances + Inverted_LW_Single_Cave_Doors)
|
||||||
@@ -833,7 +833,7 @@ def link_inverted_entrances(world, player):
|
|||||||
doors = lw_entrances + dw_entrances
|
doors = lw_entrances + dw_entrances
|
||||||
# place remaining doors
|
# place remaining doors
|
||||||
connect_doors(world, doors, door_targets, player)
|
connect_doors(world, doors, door_targets, player)
|
||||||
elif world.entrance_shuffle[player] == 'full':
|
elif world.worlds[player].options.entrance_shuffle == 'full':
|
||||||
skull_woods_shuffle(world, player)
|
skull_woods_shuffle(world, player)
|
||||||
|
|
||||||
lw_entrances = list(Inverted_LW_Entrances + Inverted_LW_Dungeon_Entrances + Inverted_LW_Single_Cave_Doors)
|
lw_entrances = list(Inverted_LW_Entrances + Inverted_LW_Dungeon_Entrances + Inverted_LW_Single_Cave_Doors)
|
||||||
@@ -984,7 +984,7 @@ def link_inverted_entrances(world, player):
|
|||||||
|
|
||||||
# place remaining doors
|
# place remaining doors
|
||||||
connect_doors(world, doors, door_targets, player)
|
connect_doors(world, doors, door_targets, player)
|
||||||
elif world.entrance_shuffle[player] == 'crossed':
|
elif world.worlds[player].options.entrance_shuffle == 'crossed':
|
||||||
skull_woods_shuffle(world, player)
|
skull_woods_shuffle(world, player)
|
||||||
|
|
||||||
entrances = list(Inverted_LW_Entrances + Inverted_LW_Dungeon_Entrances + Inverted_LW_Single_Cave_Doors + Inverted_Old_Man_Entrances + Inverted_DW_Entrances + Inverted_DW_Dungeon_Entrances + Inverted_DW_Single_Cave_Doors)
|
entrances = list(Inverted_LW_Entrances + Inverted_LW_Dungeon_Entrances + Inverted_LW_Single_Cave_Doors + Inverted_Old_Man_Entrances + Inverted_DW_Entrances + Inverted_DW_Dungeon_Entrances + Inverted_DW_Single_Cave_Doors)
|
||||||
@@ -1095,7 +1095,7 @@ def link_inverted_entrances(world, player):
|
|||||||
|
|
||||||
# place remaining doors
|
# place remaining doors
|
||||||
connect_doors(world, entrances, door_targets, player)
|
connect_doors(world, entrances, door_targets, player)
|
||||||
elif world.entrance_shuffle[player] == 'insanity':
|
elif world.worlds[player].options.entrance_shuffle == 'insanity':
|
||||||
# beware ye who enter here
|
# beware ye who enter here
|
||||||
|
|
||||||
entrances = Inverted_LW_Entrances + Inverted_LW_Dungeon_Entrances + Inverted_DW_Entrances + Inverted_DW_Dungeon_Entrances + Inverted_Old_Man_Entrances + Old_Man_Entrances + ['Skull Woods Second Section Door (East)', 'Skull Woods Second Section Door (West)', 'Skull Woods First Section Door', 'Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave', 'Hyrule Castle Entrance (South)']
|
entrances = Inverted_LW_Entrances + Inverted_LW_Dungeon_Entrances + Inverted_DW_Entrances + Inverted_DW_Dungeon_Entrances + Inverted_Old_Man_Entrances + Old_Man_Entrances + ['Skull Woods Second Section Door (East)', 'Skull Woods Second Section Door (West)', 'Skull Woods First Section Door', 'Kakariko Well Cave', 'Bat Cave Cave', 'North Fairy Cave', 'Sanctuary', 'Lost Woods Hideout Stump', 'Lumberjack Tree Cave', 'Hyrule Castle Entrance (South)']
|
||||||
@@ -1254,10 +1254,10 @@ def link_inverted_entrances(world, player):
|
|||||||
else:
|
else:
|
||||||
raise NotImplementedError('Shuffling not supported yet')
|
raise NotImplementedError('Shuffling not supported yet')
|
||||||
|
|
||||||
if world.glitches_required[player] in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']:
|
if world.worlds[player].options.glitches_required in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']:
|
||||||
overworld_glitch_connections(world, player)
|
overworld_glitch_connections(world, player)
|
||||||
# mandatory hybrid major glitches connections
|
# mandatory hybrid major glitches connections
|
||||||
if world.glitches_required[player] in ['hybrid_major_glitches', 'no_logic']:
|
if world.worlds[player].options.glitches_required in ['hybrid_major_glitches', 'no_logic']:
|
||||||
underworld_glitch_connections(world, player)
|
underworld_glitch_connections(world, player)
|
||||||
|
|
||||||
# patch swamp drain
|
# patch swamp drain
|
||||||
@@ -1349,7 +1349,7 @@ def scramble_holes(world, player):
|
|||||||
else:
|
else:
|
||||||
hole_targets.append(('Pyramid Exit', 'Pyramid'))
|
hole_targets.append(('Pyramid Exit', 'Pyramid'))
|
||||||
|
|
||||||
if world.mode[player] == 'standard':
|
if world.worlds[player].options.mode == 'standard':
|
||||||
# cannot move uncle cave
|
# cannot move uncle cave
|
||||||
connect_two_way(world, 'Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit', player)
|
connect_two_way(world, 'Hyrule Castle Secret Entrance Stairs', 'Hyrule Castle Secret Entrance Exit', player)
|
||||||
connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player)
|
connect_entrance(world, 'Hyrule Castle Secret Entrance Drop', 'Hyrule Castle Secret Entrance', player)
|
||||||
@@ -1358,14 +1358,14 @@ def scramble_holes(world, player):
|
|||||||
hole_targets.append(('Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance'))
|
hole_targets.append(('Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance'))
|
||||||
|
|
||||||
# do not shuffle sanctuary into pyramid hole unless shuffle is crossed
|
# do not shuffle sanctuary into pyramid hole unless shuffle is crossed
|
||||||
if world.entrance_shuffle[player] == 'crossed':
|
if world.worlds[player].options.entrance_shuffle == 'crossed':
|
||||||
hole_targets.append(('Sanctuary Exit', 'Sewer Drop'))
|
hole_targets.append(('Sanctuary Exit', 'Sewer Drop'))
|
||||||
if world.shuffle_ganon:
|
if world.shuffle_ganon:
|
||||||
world.random.shuffle(hole_targets)
|
world.random.shuffle(hole_targets)
|
||||||
exit, target = hole_targets.pop()
|
exit, target = hole_targets.pop()
|
||||||
connect_two_way(world, 'Pyramid Entrance', exit, player)
|
connect_two_way(world, 'Pyramid Entrance', exit, player)
|
||||||
connect_entrance(world, 'Pyramid Hole', target, player)
|
connect_entrance(world, 'Pyramid Hole', target, player)
|
||||||
if world.entrance_shuffle[player] != 'crossed':
|
if world.worlds[player].options.entrance_shuffle != 'crossed':
|
||||||
hole_targets.append(('Sanctuary Exit', 'Sewer Drop'))
|
hole_targets.append(('Sanctuary Exit', 'Sewer Drop'))
|
||||||
|
|
||||||
world.random.shuffle(hole_targets)
|
world.random.shuffle(hole_targets)
|
||||||
@@ -1400,14 +1400,14 @@ def scramble_inverted_holes(world, player):
|
|||||||
hole_targets.append(('Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance'))
|
hole_targets.append(('Hyrule Castle Secret Entrance Exit', 'Hyrule Castle Secret Entrance'))
|
||||||
|
|
||||||
# do not shuffle sanctuary into pyramid hole unless shuffle is crossed
|
# do not shuffle sanctuary into pyramid hole unless shuffle is crossed
|
||||||
if world.entrance_shuffle[player] == 'crossed':
|
if world.worlds[player].options.entrance_shuffle == 'crossed':
|
||||||
hole_targets.append(('Sanctuary Exit', 'Sewer Drop'))
|
hole_targets.append(('Sanctuary Exit', 'Sewer Drop'))
|
||||||
if world.shuffle_ganon:
|
if world.shuffle_ganon:
|
||||||
world.random.shuffle(hole_targets)
|
world.random.shuffle(hole_targets)
|
||||||
exit, target = hole_targets.pop()
|
exit, target = hole_targets.pop()
|
||||||
connect_two_way(world, 'Inverted Pyramid Entrance', exit, player)
|
connect_two_way(world, 'Inverted Pyramid Entrance', exit, player)
|
||||||
connect_entrance(world, 'Inverted Pyramid Hole', target, player)
|
connect_entrance(world, 'Inverted Pyramid Hole', target, player)
|
||||||
if world.entrance_shuffle[player] != 'crossed':
|
if world.worlds[player].options.entrance_shuffle != 'crossed':
|
||||||
hole_targets.append(('Sanctuary Exit', 'Sewer Drop'))
|
hole_targets.append(('Sanctuary Exit', 'Sewer Drop'))
|
||||||
|
|
||||||
world.random.shuffle(hole_targets)
|
world.random.shuffle(hole_targets)
|
||||||
@@ -1430,15 +1430,15 @@ def connect_random(world, exitlist, targetlist, player, two_way=False):
|
|||||||
def connect_mandatory_exits(world, entrances, caves, must_be_exits, player):
|
def connect_mandatory_exits(world, entrances, caves, must_be_exits, player):
|
||||||
|
|
||||||
# Keeps track of entrances that cannot be used to access each exit / cave
|
# Keeps track of entrances that cannot be used to access each exit / cave
|
||||||
if world.mode[player] == 'inverted':
|
if world.worlds[player].options.mode == 'inverted':
|
||||||
invalid_connections = Inverted_Must_Exit_Invalid_Connections.copy()
|
invalid_connections = Inverted_Must_Exit_Invalid_Connections.copy()
|
||||||
else:
|
else:
|
||||||
invalid_connections = Must_Exit_Invalid_Connections.copy()
|
invalid_connections = Must_Exit_Invalid_Connections.copy()
|
||||||
invalid_cave_connections = defaultdict(set)
|
invalid_cave_connections = defaultdict(set)
|
||||||
|
|
||||||
if world.glitches_required[player] in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']:
|
if world.worlds[player].options.glitches_required in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']:
|
||||||
from . import OverworldGlitchRules
|
from . import OverworldGlitchRules
|
||||||
for entrance in OverworldGlitchRules.get_non_mandatory_exits(world.mode[player] == 'inverted'):
|
for entrance in OverworldGlitchRules.get_non_mandatory_exits(world.worlds[player].options.mode == 'inverted'):
|
||||||
invalid_connections[entrance] = set()
|
invalid_connections[entrance] = set()
|
||||||
if entrance in must_be_exits:
|
if entrance in must_be_exits:
|
||||||
must_be_exits.remove(entrance)
|
must_be_exits.remove(entrance)
|
||||||
@@ -1449,7 +1449,7 @@ def connect_mandatory_exits(world, entrances, caves, must_be_exits, player):
|
|||||||
world.random.shuffle(caves)
|
world.random.shuffle(caves)
|
||||||
|
|
||||||
# Handle inverted Aga Tower - if it depends on connections, then so does Hyrule Castle Ledge
|
# Handle inverted Aga Tower - if it depends on connections, then so does Hyrule Castle Ledge
|
||||||
if world.mode[player] == 'inverted':
|
if world.worlds[player].options.mode == 'inverted':
|
||||||
for entrance in invalid_connections:
|
for entrance in invalid_connections:
|
||||||
if world.get_entrance(entrance, player).connected_region == world.get_region('Inverted Agahnims Tower',
|
if world.get_entrance(entrance, player).connected_region == world.get_region('Inverted Agahnims Tower',
|
||||||
player):
|
player):
|
||||||
@@ -1490,7 +1490,7 @@ def connect_mandatory_exits(world, entrances, caves, must_be_exits, player):
|
|||||||
entrance = next(e for e in entrances[::-1] if e not in invalid_connections[exit])
|
entrance = next(e for e in entrances[::-1] if e not in invalid_connections[exit])
|
||||||
cave_entrances.append(entrance)
|
cave_entrances.append(entrance)
|
||||||
entrances.remove(entrance)
|
entrances.remove(entrance)
|
||||||
connect_two_way(world,entrance,cave_exit, player)
|
connect_two_way(world, entrance, cave_exit, player)
|
||||||
if entrance not in invalid_connections:
|
if entrance not in invalid_connections:
|
||||||
invalid_connections[exit] = set()
|
invalid_connections[exit] = set()
|
||||||
if all(entrance in invalid_connections for entrance in cave_entrances):
|
if all(entrance in invalid_connections for entrance in cave_entrances):
|
||||||
@@ -1564,7 +1564,7 @@ def simple_shuffle_dungeons(world, player):
|
|||||||
dungeon_entrances = ['Eastern Palace', 'Tower of Hera', 'Thieves Town', 'Skull Woods Final Section', 'Palace of Darkness', 'Ice Palace', 'Misery Mire', 'Swamp Palace']
|
dungeon_entrances = ['Eastern Palace', 'Tower of Hera', 'Thieves Town', 'Skull Woods Final Section', 'Palace of Darkness', 'Ice Palace', 'Misery Mire', 'Swamp Palace']
|
||||||
dungeon_exits = ['Eastern Palace Exit', 'Tower of Hera Exit', 'Thieves Town Exit', 'Skull Woods Final Section Exit', 'Palace of Darkness Exit', 'Ice Palace Exit', 'Misery Mire Exit', 'Swamp Palace Exit']
|
dungeon_exits = ['Eastern Palace Exit', 'Tower of Hera Exit', 'Thieves Town Exit', 'Skull Woods Final Section Exit', 'Palace of Darkness Exit', 'Ice Palace Exit', 'Misery Mire Exit', 'Swamp Palace Exit']
|
||||||
|
|
||||||
if world.mode[player] != 'inverted':
|
if world.worlds[player].options.mode != 'inverted':
|
||||||
if not world.shuffle_ganon:
|
if not world.shuffle_ganon:
|
||||||
connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player)
|
connect_two_way(world, 'Ganons Tower', 'Ganons Tower Exit', player)
|
||||||
else:
|
else:
|
||||||
@@ -1579,13 +1579,13 @@ def simple_shuffle_dungeons(world, player):
|
|||||||
|
|
||||||
# mix up 4 door dungeons
|
# mix up 4 door dungeons
|
||||||
multi_dungeons = ['Desert', 'Turtle Rock']
|
multi_dungeons = ['Desert', 'Turtle Rock']
|
||||||
if world.mode[player] == 'open' or (world.mode[player] == 'inverted' and world.shuffle_ganon):
|
if world.worlds[player].options.mode == 'open' or (world.worlds[player].options.mode == 'inverted' and world.shuffle_ganon):
|
||||||
multi_dungeons.append('Hyrule Castle')
|
multi_dungeons.append('Hyrule Castle')
|
||||||
world.random.shuffle(multi_dungeons)
|
world.random.shuffle(multi_dungeons)
|
||||||
|
|
||||||
dp_target = multi_dungeons[0]
|
dp_target = multi_dungeons[0]
|
||||||
tr_target = multi_dungeons[1]
|
tr_target = multi_dungeons[1]
|
||||||
if world.mode[player] not in ['open', 'inverted'] or (world.mode[player] == 'inverted' and world.shuffle_ganon is False):
|
if world.worlds[player].options.mode not in ['open', 'inverted'] or (world.worlds[player].options.mode == 'inverted' and world.shuffle_ganon is False):
|
||||||
# place hyrule castle as intended
|
# place hyrule castle as intended
|
||||||
hc_target = 'Hyrule Castle'
|
hc_target = 'Hyrule Castle'
|
||||||
else:
|
else:
|
||||||
@@ -1593,7 +1593,7 @@ def simple_shuffle_dungeons(world, player):
|
|||||||
|
|
||||||
# ToDo improve this?
|
# ToDo improve this?
|
||||||
|
|
||||||
if world.mode[player] != 'inverted':
|
if world.worlds[player].options.mode != 'inverted':
|
||||||
if hc_target == 'Hyrule Castle':
|
if hc_target == 'Hyrule Castle':
|
||||||
connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
|
connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
|
||||||
connect_two_way(world, 'Hyrule Castle Entrance (East)', 'Hyrule Castle Exit (East)', player)
|
connect_two_way(world, 'Hyrule Castle Entrance (East)', 'Hyrule Castle Exit (East)', player)
|
||||||
@@ -1708,7 +1708,7 @@ def crossed_shuffle_dungeons(world, player: int):
|
|||||||
dungeon_entrances.append('Ganons Tower')
|
dungeon_entrances.append('Ganons Tower')
|
||||||
dungeon_exits.append('Ganons Tower Exit')
|
dungeon_exits.append('Ganons Tower Exit')
|
||||||
|
|
||||||
if world.mode[player] == 'standard':
|
if world.worlds[player].options.mode == 'standard':
|
||||||
# must connect front of hyrule castle to do escape
|
# must connect front of hyrule castle to do escape
|
||||||
connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
|
connect_two_way(world, 'Hyrule Castle Entrance (South)', 'Hyrule Castle Exit (South)', player)
|
||||||
else:
|
else:
|
||||||
@@ -1718,7 +1718,7 @@ def crossed_shuffle_dungeons(world, player: int):
|
|||||||
connect_mandatory_exits(world, dungeon_entrances, dungeon_exits,
|
connect_mandatory_exits(world, dungeon_entrances, dungeon_exits,
|
||||||
LW_Dungeon_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit, player)
|
LW_Dungeon_Entrances_Must_Exit + DW_Dungeon_Entrances_Must_Exit, player)
|
||||||
|
|
||||||
if world.mode[player] == 'standard':
|
if world.worlds[player].options.mode == 'standard':
|
||||||
connect_caves(world, dungeon_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player)
|
connect_caves(world, dungeon_entrances, [], [('Hyrule Castle Exit (West)', 'Hyrule Castle Exit (East)')], player)
|
||||||
|
|
||||||
connect_caves(world, dungeon_entrances, [], dungeon_exits, player)
|
connect_caves(world, dungeon_entrances, [], dungeon_exits, player)
|
||||||
@@ -1823,14 +1823,14 @@ lookup = {
|
|||||||
|
|
||||||
|
|
||||||
def plando_connect(world, player: int):
|
def plando_connect(world, player: int):
|
||||||
if world.plando_connections[player]:
|
if world.worlds[player].options.plando_connections:
|
||||||
for connection in world.plando_connections[player]:
|
for connection in world.worlds[player].options.plando_connections:
|
||||||
func = lookup[connection.direction]
|
func = lookup[connection.direction]
|
||||||
try:
|
try:
|
||||||
func(world, connection.entrance, connection.exit, player)
|
func(world, connection.entrance, connection.exit, player)
|
||||||
except Exception as e:
|
except Exception as e:
|
||||||
raise Exception(f"Could not connect using {connection}") from e
|
raise Exception(f"Could not connect using {connection}") from e
|
||||||
if world.mode[player] != 'inverted':
|
if world.worlds[player].options.mode != 'inverted':
|
||||||
mark_light_world_regions(world, player)
|
mark_light_world_regions(world, player)
|
||||||
else:
|
else:
|
||||||
mark_dark_world_regions(world, player)
|
mark_dark_world_regions(world, player)
|
||||||
|
|||||||
@@ -226,25 +226,25 @@ def generate_itempool(world):
|
|||||||
player = world.player
|
player = world.player
|
||||||
multiworld = world.multiworld
|
multiworld = world.multiworld
|
||||||
|
|
||||||
if multiworld.item_pool[player].current_key not in difficulties:
|
if world.options.item_pool.current_key not in difficulties:
|
||||||
raise NotImplementedError(f"Diffulty {multiworld.item_pool[player]}")
|
raise NotImplementedError(f"Diffulty {world.options.item_pool}")
|
||||||
if multiworld.goal[player] not in ('ganon', 'pedestal', 'bosses', 'triforce_hunt', 'local_triforce_hunt',
|
if world.options.goal not in ('ganon', 'pedestal', 'bosses', 'triforce_hunt', 'local_triforce_hunt',
|
||||||
'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'crystals',
|
'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'crystals',
|
||||||
'ganon_pedestal'):
|
'ganon_pedestal'):
|
||||||
raise NotImplementedError(f"Goal {multiworld.goal[player]} for player {player}")
|
raise NotImplementedError(f"Goal {world.options.goal} for player {player}")
|
||||||
if multiworld.mode[player] not in ('open', 'standard', 'inverted'):
|
if world.options.mode not in ('open', 'standard', 'inverted'):
|
||||||
raise NotImplementedError(f"Mode {multiworld.mode[player]} for player {player}")
|
raise NotImplementedError(f"Mode {world.options.mode} for player {player}")
|
||||||
if multiworld.timer[player] not in (False, 'display', 'timed', 'timed_ohko', 'ohko', 'timed_countdown'):
|
if world.options.timer not in (False, 'display', 'timed', 'timed_ohko', 'ohko', 'timed_countdown'):
|
||||||
raise NotImplementedError(f"Timer {multiworld.timer[player]} for player {player}")
|
raise NotImplementedError(f"Timer {world.options.timer} for player {player}")
|
||||||
|
|
||||||
if multiworld.timer[player] in ['ohko', 'timed_ohko']:
|
if world.options.timer in ['ohko', 'timed_ohko']:
|
||||||
world.can_take_damage = False
|
world.can_take_damage = False
|
||||||
if multiworld.goal[player] in ['pedestal', 'triforce_hunt', 'local_triforce_hunt']:
|
if world.options.goal in ['pedestal', 'triforce_hunt', 'local_triforce_hunt']:
|
||||||
multiworld.push_item(multiworld.get_location('Ganon', player), item_factory('Nothing', world), False)
|
multiworld.push_item(multiworld.get_location('Ganon', player), item_factory('Nothing', world), False)
|
||||||
else:
|
else:
|
||||||
multiworld.push_item(multiworld.get_location('Ganon', player), item_factory('Triforce', world), False)
|
multiworld.push_item(multiworld.get_location('Ganon', player), item_factory('Triforce', world), False)
|
||||||
|
|
||||||
if multiworld.goal[player] in ['triforce_hunt', 'local_triforce_hunt']:
|
if world.options.goal in ['triforce_hunt', 'local_triforce_hunt']:
|
||||||
region = multiworld.get_region('Light World', player)
|
region = multiworld.get_region('Light World', player)
|
||||||
|
|
||||||
loc = ALttPLocation(player, "Murahdahla", parent=region)
|
loc = ALttPLocation(player, "Murahdahla", parent=region)
|
||||||
@@ -288,7 +288,7 @@ def generate_itempool(world):
|
|||||||
for item in precollected_items:
|
for item in precollected_items:
|
||||||
multiworld.push_precollected(item_factory(item, world))
|
multiworld.push_precollected(item_factory(item, world))
|
||||||
|
|
||||||
if multiworld.mode[player] == 'standard' and not has_melee_weapon(multiworld.state, player):
|
if world.options.mode == 'standard' and not has_melee_weapon(multiworld.state, player):
|
||||||
if "Link's Uncle" not in placed_items:
|
if "Link's Uncle" not in placed_items:
|
||||||
found_sword = False
|
found_sword = False
|
||||||
found_bow = False
|
found_bow = False
|
||||||
@@ -304,10 +304,10 @@ def generate_itempool(world):
|
|||||||
elif item in ['Hammer', 'Fire Rod', 'Cane of Somaria', 'Cane of Byrna']:
|
elif item in ['Hammer', 'Fire Rod', 'Cane of Somaria', 'Cane of Byrna']:
|
||||||
if item not in possible_weapons:
|
if item not in possible_weapons:
|
||||||
possible_weapons.append(item)
|
possible_weapons.append(item)
|
||||||
elif (item == 'Bombs (10)' and (not multiworld.bombless_start[player]) and item not in
|
elif (item == 'Bombs (10)' and (not world.options.bombless_start) and item not in
|
||||||
possible_weapons):
|
possible_weapons):
|
||||||
possible_weapons.append(item)
|
possible_weapons.append(item)
|
||||||
elif (item in ['Bomb Upgrade (+10)', 'Bomb Upgrade (50)'] and multiworld.bombless_start[player] and item
|
elif (item in ['Bomb Upgrade (+10)', 'Bomb Upgrade (50)'] and world.options.bombless_start and item
|
||||||
not in possible_weapons):
|
not in possible_weapons):
|
||||||
possible_weapons.append(item)
|
possible_weapons.append(item)
|
||||||
|
|
||||||
@@ -315,21 +315,21 @@ def generate_itempool(world):
|
|||||||
placed_items["Link's Uncle"] = starting_weapon
|
placed_items["Link's Uncle"] = starting_weapon
|
||||||
pool.remove(starting_weapon)
|
pool.remove(starting_weapon)
|
||||||
if (placed_items["Link's Uncle"] in ['Bow', 'Progressive Bow', 'Bombs (10)', 'Bomb Upgrade (+10)',
|
if (placed_items["Link's Uncle"] in ['Bow', 'Progressive Bow', 'Bombs (10)', 'Bomb Upgrade (+10)',
|
||||||
'Bomb Upgrade (50)', 'Cane of Somaria', 'Cane of Byrna'] and multiworld.enemy_health[player] not in ['default', 'easy']):
|
'Bomb Upgrade (50)', 'Cane of Somaria', 'Cane of Byrna'] and world.options.enemy_health not in ['default', 'easy']):
|
||||||
if multiworld.bombless_start[player] and "Bomb Upgrade" not in placed_items["Link's Uncle"]:
|
if world.options.bombless_start and "Bomb Upgrade" not in placed_items["Link's Uncle"]:
|
||||||
if 'Bow' in placed_items["Link's Uncle"]:
|
if 'Bow' in placed_items["Link's Uncle"]:
|
||||||
multiworld.worlds[player].escape_assist.append('arrows')
|
world.escape_assist.append('arrows')
|
||||||
elif 'Cane' in placed_items["Link's Uncle"]:
|
elif 'Cane' in placed_items["Link's Uncle"]:
|
||||||
multiworld.worlds[player].escape_assist.append('magic')
|
world.escape_assist.append('magic')
|
||||||
else:
|
else:
|
||||||
multiworld.worlds[player].escape_assist.append('bombs')
|
world.escape_assist.append('bombs')
|
||||||
|
|
||||||
for (location, item) in placed_items.items():
|
for (location, item) in placed_items.items():
|
||||||
multiworld.get_location(location, player).place_locked_item(item_factory(item, world))
|
multiworld.get_location(location, player).place_locked_item(item_factory(item, world))
|
||||||
|
|
||||||
items = item_factory(pool, world)
|
items = item_factory(pool, world)
|
||||||
# convert one Progressive Bow into Progressive Bow (Alt), in ID only, for ganon silvers hint text
|
# convert one Progressive Bow into Progressive Bow (Alt), in ID only, for ganon silvers hint text
|
||||||
if multiworld.worlds[player].has_progressive_bows:
|
if world.has_progressive_bows:
|
||||||
for item in items:
|
for item in items:
|
||||||
if item.code == 0x64: # Progressive Bow
|
if item.code == 0x64: # Progressive Bow
|
||||||
item.code = 0x65 # Progressive Bow (Alt)
|
item.code = 0x65 # Progressive Bow (Alt)
|
||||||
@@ -338,21 +338,21 @@ def generate_itempool(world):
|
|||||||
if clock_mode:
|
if clock_mode:
|
||||||
world.clock_mode = clock_mode
|
world.clock_mode = clock_mode
|
||||||
|
|
||||||
multiworld.worlds[player].treasure_hunt_required = treasure_hunt_required % 999
|
world.treasure_hunt_required = treasure_hunt_required % 999
|
||||||
multiworld.worlds[player].treasure_hunt_total = treasure_hunt_total
|
world.treasure_hunt_total = treasure_hunt_total
|
||||||
|
|
||||||
dungeon_items = [item for item in get_dungeon_item_pool_player(world)
|
dungeon_items = [item for item in get_dungeon_item_pool_player(world)
|
||||||
if item.name not in multiworld.worlds[player].dungeon_local_item_names]
|
if item.name not in world.dungeon_local_item_names]
|
||||||
|
|
||||||
for key_loc in key_drop_data:
|
for key_loc in key_drop_data:
|
||||||
key_data = key_drop_data[key_loc]
|
key_data = key_drop_data[key_loc]
|
||||||
drop_item = item_factory(key_data[3], world)
|
drop_item = item_factory(key_data[3], world)
|
||||||
if not multiworld.key_drop_shuffle[player]:
|
if not world.options.key_drop_shuffle:
|
||||||
if drop_item in dungeon_items:
|
if drop_item in dungeon_items:
|
||||||
dungeon_items.remove(drop_item)
|
dungeon_items.remove(drop_item)
|
||||||
else:
|
else:
|
||||||
dungeon = drop_item.name.split("(")[1].split(")")[0]
|
dungeon = drop_item.name.split("(")[1].split(")")[0]
|
||||||
if multiworld.mode[player] == 'inverted':
|
if world.options.mode == 'inverted':
|
||||||
if dungeon == "Agahnims Tower":
|
if dungeon == "Agahnims Tower":
|
||||||
dungeon = "Inverted Agahnims Tower"
|
dungeon = "Inverted Agahnims Tower"
|
||||||
if dungeon == "Ganons Tower":
|
if dungeon == "Ganons Tower":
|
||||||
@@ -365,7 +365,7 @@ def generate_itempool(world):
|
|||||||
loc = multiworld.get_location(key_loc, player)
|
loc = multiworld.get_location(key_loc, player)
|
||||||
loc.place_locked_item(drop_item)
|
loc.place_locked_item(drop_item)
|
||||||
loc.address = None
|
loc.address = None
|
||||||
elif "Small" in key_data[3] and multiworld.small_key_shuffle[player] == small_key_shuffle.option_universal:
|
elif "Small" in key_data[3] and world.options.small_key_shuffle == small_key_shuffle.option_universal:
|
||||||
# key drop shuffle and universal keys are on. Add universal keys in place of key drop keys.
|
# key drop shuffle and universal keys are on. Add universal keys in place of key drop keys.
|
||||||
multiworld.itempool.append(item_factory(GetBeemizerItem(multiworld, player, 'Small Key (Universal)'), world))
|
multiworld.itempool.append(item_factory(GetBeemizerItem(multiworld, player, 'Small Key (Universal)'), world))
|
||||||
dungeon_item_replacements = sum(difficulties[world.options.item_pool.current_key].extras, []) * 2
|
dungeon_item_replacements = sum(difficulties[world.options.item_pool.current_key].extras, []) * 2
|
||||||
@@ -373,10 +373,10 @@ def generate_itempool(world):
|
|||||||
|
|
||||||
for x in range(len(dungeon_items)-1, -1, -1):
|
for x in range(len(dungeon_items)-1, -1, -1):
|
||||||
item = dungeon_items[x]
|
item = dungeon_items[x]
|
||||||
if ((multiworld.small_key_shuffle[player] == small_key_shuffle.option_start_with and item.type == 'SmallKey')
|
if ((world.options.small_key_shuffle == small_key_shuffle.option_start_with and item.type == 'SmallKey')
|
||||||
or (multiworld.big_key_shuffle[player] == big_key_shuffle.option_start_with and item.type == 'BigKey')
|
or (world.options.big_key_shuffle == big_key_shuffle.option_start_with and item.type == 'BigKey')
|
||||||
or (multiworld.compass_shuffle[player] == compass_shuffle.option_start_with and item.type == 'Compass')
|
or (world.options.compass_shuffle == compass_shuffle.option_start_with and item.type == 'Compass')
|
||||||
or (multiworld.map_shuffle[player] == map_shuffle.option_start_with and item.type == 'Map')):
|
or (world.options.map_shuffle == map_shuffle.option_start_with and item.type == 'Map')):
|
||||||
dungeon_items.pop(x)
|
dungeon_items.pop(x)
|
||||||
multiworld.push_precollected(item)
|
multiworld.push_precollected(item)
|
||||||
multiworld.itempool.append(item_factory(dungeon_item_replacements.pop(), world))
|
multiworld.itempool.append(item_factory(dungeon_item_replacements.pop(), world))
|
||||||
@@ -384,7 +384,7 @@ def generate_itempool(world):
|
|||||||
|
|
||||||
set_up_shops(multiworld, player)
|
set_up_shops(multiworld, player)
|
||||||
|
|
||||||
if multiworld.retro_bow[player]:
|
if world.options.retro_bow:
|
||||||
shop_items = 0
|
shop_items = 0
|
||||||
shop_locations = [location for shop_locations in (shop.region.locations for shop in multiworld.shops if
|
shop_locations = [location for shop_locations in (shop.region.locations for shop in multiworld.shops if
|
||||||
shop.type == ShopType.Shop and shop.region.player == player) for location in shop_locations if
|
shop.type == ShopType.Shop and shop.region.player == player) for location in shop_locations if
|
||||||
@@ -395,12 +395,12 @@ def generate_itempool(world):
|
|||||||
else:
|
else:
|
||||||
shop_items += 1
|
shop_items += 1
|
||||||
else:
|
else:
|
||||||
shop_items = min(multiworld.shop_item_slots[player], 30 if multiworld.include_witch_hut[player] else 27)
|
shop_items = min(world.options.shop_item_slots, 30 if world.options.include_witch_hut else 27)
|
||||||
|
|
||||||
if multiworld.shuffle_capacity_upgrades[player]:
|
if world.options.shuffle_capacity_upgrades:
|
||||||
shop_items += 2
|
shop_items += 2
|
||||||
chance_100 = int(multiworld.retro_bow[player]) * 0.25 + int(
|
chance_100 = int(world.options.retro_bow) * 0.25 + int(
|
||||||
multiworld.small_key_shuffle[player] == small_key_shuffle.option_universal) * 0.5
|
world.options.small_key_shuffle == small_key_shuffle.option_universal) * 0.5
|
||||||
for _ in range(shop_items):
|
for _ in range(shop_items):
|
||||||
if multiworld.random.random() < chance_100:
|
if multiworld.random.random() < chance_100:
|
||||||
items.append(item_factory(GetBeemizerItem(multiworld, player, "Rupees (100)"), world))
|
items.append(item_factory(GetBeemizerItem(multiworld, player, "Rupees (100)"), world))
|
||||||
@@ -410,19 +410,19 @@ def generate_itempool(world):
|
|||||||
multiworld.random.shuffle(items)
|
multiworld.random.shuffle(items)
|
||||||
pool_count = len(items)
|
pool_count = len(items)
|
||||||
new_items = ["Triforce Piece" for _ in range(additional_triforce_pieces)]
|
new_items = ["Triforce Piece" for _ in range(additional_triforce_pieces)]
|
||||||
if multiworld.shuffle_capacity_upgrades[player] or multiworld.bombless_start[player]:
|
if world.options.shuffle_capacity_upgrades or world.options.bombless_start:
|
||||||
progressive = multiworld.progressive[player]
|
progressive = world.options.progressive
|
||||||
progressive = multiworld.random.choice([True, False]) if progressive == 'grouped_random' else progressive == 'on'
|
progressive = multiworld.random.choice([True, False]) if progressive == 'grouped_random' else progressive == 'on'
|
||||||
if multiworld.shuffle_capacity_upgrades[player] == "on_combined":
|
if world.options.shuffle_capacity_upgrades == "on_combined":
|
||||||
new_items.append("Bomb Upgrade (50)")
|
new_items.append("Bomb Upgrade (50)")
|
||||||
elif multiworld.shuffle_capacity_upgrades[player] == "on":
|
elif world.options.shuffle_capacity_upgrades == "on":
|
||||||
new_items += ["Bomb Upgrade (+5)"] * 6
|
new_items += ["Bomb Upgrade (+5)"] * 6
|
||||||
new_items.append("Bomb Upgrade (+5)" if progressive else "Bomb Upgrade (+10)")
|
new_items.append("Bomb Upgrade (+5)" if progressive else "Bomb Upgrade (+10)")
|
||||||
if multiworld.shuffle_capacity_upgrades[player] != "on_combined" and multiworld.bombless_start[player]:
|
if world.options.shuffle_capacity_upgrades != "on_combined" and world.options.bombless_start:
|
||||||
new_items.append("Bomb Upgrade (+5)" if progressive else "Bomb Upgrade (+10)")
|
new_items.append("Bomb Upgrade (+5)" if progressive else "Bomb Upgrade (+10)")
|
||||||
|
|
||||||
if multiworld.shuffle_capacity_upgrades[player] and not multiworld.retro_bow[player]:
|
if world.options.shuffle_capacity_upgrades and not world.options.retro_bow:
|
||||||
if multiworld.shuffle_capacity_upgrades[player] == "on_combined":
|
if world.options.shuffle_capacity_upgrades == "on_combined":
|
||||||
new_items += ["Arrow Upgrade (70)"]
|
new_items += ["Arrow Upgrade (70)"]
|
||||||
else:
|
else:
|
||||||
new_items += ["Arrow Upgrade (+5)"] * 6
|
new_items += ["Arrow Upgrade (+5)"] * 6
|
||||||
@@ -481,7 +481,7 @@ def generate_itempool(world):
|
|||||||
if len(items) < pool_count:
|
if len(items) < pool_count:
|
||||||
items += removed_filler[len(items) - pool_count:]
|
items += removed_filler[len(items) - pool_count:]
|
||||||
|
|
||||||
if multiworld.randomize_cost_types[player]:
|
if world.options.randomize_cost_types:
|
||||||
# Heart and Arrow costs require all Heart Container/Pieces and Arrow Upgrades to be advancement items for logic
|
# Heart and Arrow costs require all Heart Container/Pieces and Arrow Upgrades to be advancement items for logic
|
||||||
for item in items:
|
for item in items:
|
||||||
if item.name in ("Boss Heart Container", "Sanctuary Heart Container", "Piece of Heart"):
|
if item.name in ("Boss Heart Container", "Sanctuary Heart Container", "Piece of Heart"):
|
||||||
@@ -490,21 +490,25 @@ def generate_itempool(world):
|
|||||||
# Otherwise, logic has some branches where having 4 hearts is one possible requirement (of several alternatives)
|
# Otherwise, logic has some branches where having 4 hearts is one possible requirement (of several alternatives)
|
||||||
# rather than making all hearts/heart pieces progression items (which slows down generation considerably)
|
# rather than making all hearts/heart pieces progression items (which slows down generation considerably)
|
||||||
# We mark one random heart container as an advancement item (or 4 heart pieces in expert mode)
|
# We mark one random heart container as an advancement item (or 4 heart pieces in expert mode)
|
||||||
if multiworld.item_pool[player] in ['easy', 'normal', 'hard'] and not (multiworld.custom and multiworld.customitemarray[30] == 0):
|
try:
|
||||||
next(item for item in items if item.name == 'Boss Heart Container').classification = ItemClassification.progression
|
next(item for item in items if item.name == 'Boss Heart Container').classification \
|
||||||
elif multiworld.item_pool[player] in ['expert'] and not (multiworld.custom and multiworld.customitemarray[29] < 4):
|
|= ItemClassification.progression
|
||||||
|
except StopIteration:
|
||||||
adv_heart_pieces = (item for item in items if item.name == 'Piece of Heart')
|
adv_heart_pieces = (item for item in items if item.name == 'Piece of Heart')
|
||||||
for i in range(4):
|
for i in range(4):
|
||||||
next(adv_heart_pieces).classification = ItemClassification.progression
|
try:
|
||||||
|
next(adv_heart_pieces).classification |= ItemClassification.progression
|
||||||
|
except StopIteration:
|
||||||
|
break # logically health tanking is an option, so rules should still resolve to something beatable
|
||||||
|
|
||||||
world.required_medallions = (multiworld.misery_mire_medallion[player].current_key.title(),
|
world.required_medallions = (world.options.misery_mire_medallion.current_key.title(),
|
||||||
multiworld.turtle_rock_medallion[player].current_key.title())
|
world.options.turtle_rock_medallion.current_key.title())
|
||||||
|
|
||||||
place_bosses(world)
|
place_bosses(world)
|
||||||
|
|
||||||
multiworld.itempool += items
|
multiworld.itempool += items
|
||||||
|
|
||||||
if multiworld.retro_caves[player]:
|
if world.options.retro_caves:
|
||||||
set_up_take_anys(multiworld, world, player) # depends on world.itempool to be set
|
set_up_take_anys(multiworld, world, player) # depends on world.itempool to be set
|
||||||
|
|
||||||
|
|
||||||
@@ -527,7 +531,7 @@ take_any_locations.sort()
|
|||||||
|
|
||||||
def set_up_take_anys(multiworld, world, player):
|
def set_up_take_anys(multiworld, world, player):
|
||||||
# these are references, do not modify these lists in-place
|
# these are references, do not modify these lists in-place
|
||||||
if multiworld.mode[player] == 'inverted':
|
if world.options.mode == 'inverted':
|
||||||
take_any_locs = take_any_locations_inverted
|
take_any_locs = take_any_locations_inverted
|
||||||
else:
|
else:
|
||||||
take_any_locs = take_any_locations
|
take_any_locs = take_any_locations
|
||||||
@@ -578,14 +582,14 @@ def set_up_take_anys(multiworld, world, player):
|
|||||||
|
|
||||||
|
|
||||||
def get_pool_core(world, player: int):
|
def get_pool_core(world, player: int):
|
||||||
shuffle = world.entrance_shuffle[player].current_key
|
shuffle = world.worlds[player].options.entrance_shuffle.current_key
|
||||||
difficulty = world.item_pool[player].current_key
|
difficulty = world.worlds[player].options.item_pool.current_key
|
||||||
timer = world.timer[player].current_key
|
timer = world.worlds[player].options.timer.current_key
|
||||||
goal = world.goal[player].current_key
|
goal = world.worlds[player].options.goal.current_key
|
||||||
mode = world.mode[player].current_key
|
mode = world.worlds[player].options.mode.current_key
|
||||||
swordless = world.swordless[player]
|
swordless = world.worlds[player].options.swordless
|
||||||
retro_bow = world.retro_bow[player]
|
retro_bow = world.worlds[player].options.retro_bow
|
||||||
logic = world.glitches_required[player]
|
logic = world.worlds[player].options.glitches_required
|
||||||
|
|
||||||
pool = []
|
pool = []
|
||||||
placed_items = {}
|
placed_items = {}
|
||||||
@@ -602,11 +606,11 @@ def get_pool_core(world, player: int):
|
|||||||
placed_items[loc] = item
|
placed_items[loc] = item
|
||||||
|
|
||||||
# provide boots to major glitch dependent seeds
|
# provide boots to major glitch dependent seeds
|
||||||
if logic.current_key in {'overworld_glitches', 'hybrid_major_glitches', 'no_logic'} and world.glitch_boots[player]:
|
if logic.current_key in {'overworld_glitches', 'hybrid_major_glitches', 'no_logic'} and world.worlds[player].options.glitch_boots:
|
||||||
precollected_items.append('Pegasus Boots')
|
precollected_items.append('Pegasus Boots')
|
||||||
pool.remove('Pegasus Boots')
|
pool.remove('Pegasus Boots')
|
||||||
pool.append('Rupees (20)')
|
pool.append('Rupees (20)')
|
||||||
want_progressives = world.progressive[player].want_progressives
|
want_progressives = world.worlds[player].options.progressive.want_progressives
|
||||||
|
|
||||||
if want_progressives(world.random):
|
if want_progressives(world.random):
|
||||||
pool.extend(diff.progressiveglove)
|
pool.extend(diff.progressiveglove)
|
||||||
@@ -680,22 +684,22 @@ def get_pool_core(world, player: int):
|
|||||||
additional_pieces_to_place = 0
|
additional_pieces_to_place = 0
|
||||||
if 'triforce_hunt' in goal:
|
if 'triforce_hunt' in goal:
|
||||||
|
|
||||||
if world.triforce_pieces_mode[player].value == TriforcePiecesMode.option_extra:
|
if world.worlds[player].options.triforce_pieces_mode.value == TriforcePiecesMode.option_extra:
|
||||||
treasure_hunt_total = (world.triforce_pieces_required[player].value
|
treasure_hunt_total = (world.worlds[player].options.triforce_pieces_required.value
|
||||||
+ world.triforce_pieces_extra[player].value)
|
+ world.worlds[player].options.triforce_pieces_extra.value)
|
||||||
elif world.triforce_pieces_mode[player].value == TriforcePiecesMode.option_percentage:
|
elif world.worlds[player].options.triforce_pieces_mode.value == TriforcePiecesMode.option_percentage:
|
||||||
percentage = float(world.triforce_pieces_percentage[player].value) / 100
|
percentage = float(world.worlds[player].options.triforce_pieces_percentage.value) / 100
|
||||||
treasure_hunt_total = int(round(world.triforce_pieces_required[player].value * percentage, 0))
|
treasure_hunt_total = int(round(world.worlds[player].options.triforce_pieces_required.value * percentage, 0))
|
||||||
else: # available
|
else: # available
|
||||||
treasure_hunt_total = world.triforce_pieces_available[player].value
|
treasure_hunt_total = world.worlds[player].options.triforce_pieces_available.value
|
||||||
|
|
||||||
triforce_pieces = min(90, max(treasure_hunt_total, world.triforce_pieces_required[player].value))
|
triforce_pieces = min(90, max(treasure_hunt_total, world.worlds[player].options.triforce_pieces_required.value))
|
||||||
|
|
||||||
pieces_in_core = min(extraitems, triforce_pieces)
|
pieces_in_core = min(extraitems, triforce_pieces)
|
||||||
additional_pieces_to_place = triforce_pieces - pieces_in_core
|
additional_pieces_to_place = triforce_pieces - pieces_in_core
|
||||||
pool.extend(["Triforce Piece"] * pieces_in_core)
|
pool.extend(["Triforce Piece"] * pieces_in_core)
|
||||||
extraitems -= pieces_in_core
|
extraitems -= pieces_in_core
|
||||||
treasure_hunt_required = world.triforce_pieces_required[player].value
|
treasure_hunt_required = world.worlds[player].options.triforce_pieces_required.value
|
||||||
|
|
||||||
for extra in diff.extras:
|
for extra in diff.extras:
|
||||||
if extraitems >= len(extra):
|
if extraitems >= len(extra):
|
||||||
@@ -707,17 +711,24 @@ def get_pool_core(world, player: int):
|
|||||||
else:
|
else:
|
||||||
break
|
break
|
||||||
|
|
||||||
if goal == 'pedestal':
|
|
||||||
place_item('Master Sword Pedestal', 'Triforce')
|
|
||||||
pool.remove("Rupees (20)")
|
|
||||||
|
|
||||||
if retro_bow:
|
if retro_bow:
|
||||||
replace = {'Single Arrow', 'Arrows (10)', 'Arrow Upgrade (+5)', 'Arrow Upgrade (+10)', 'Arrow Upgrade (70)'}
|
replace = {'Single Arrow', 'Arrows (10)', 'Arrow Upgrade (+5)', 'Arrow Upgrade (+10)', 'Arrow Upgrade (70)'}
|
||||||
pool = ['Rupees (5)' if item in replace else item for item in pool]
|
pool = ['Rupees (5)' if item in replace else item for item in pool]
|
||||||
if world.small_key_shuffle[player] == small_key_shuffle.option_universal:
|
|
||||||
|
if goal == 'pedestal':
|
||||||
|
place_item('Master Sword Pedestal', 'Triforce')
|
||||||
|
for rupee_name in ("Rupees (5)", "Rupees (20)", "Rupees (50)", "Rupees (100)", "Rupees (300)"):
|
||||||
|
try:
|
||||||
|
pool.remove(rupee_name)
|
||||||
|
except ValueError:
|
||||||
|
pass
|
||||||
|
else:
|
||||||
|
break
|
||||||
|
|
||||||
|
if world.worlds[player].options.small_key_shuffle == small_key_shuffle.option_universal:
|
||||||
pool.extend(diff.universal_keys)
|
pool.extend(diff.universal_keys)
|
||||||
if mode == 'standard':
|
if mode == 'standard':
|
||||||
if world.key_drop_shuffle[player]:
|
if world.worlds[player].options.key_drop_shuffle:
|
||||||
key_locations = ['Secret Passage', 'Hyrule Castle - Map Guard Key Drop']
|
key_locations = ['Secret Passage', 'Hyrule Castle - Map Guard Key Drop']
|
||||||
key_location = world.random.choice(key_locations)
|
key_location = world.random.choice(key_locations)
|
||||||
key_locations.remove(key_location)
|
key_locations.remove(key_location)
|
||||||
@@ -741,11 +752,11 @@ def get_pool_core(world, player: int):
|
|||||||
|
|
||||||
|
|
||||||
def make_custom_item_pool(world, player):
|
def make_custom_item_pool(world, player):
|
||||||
shuffle = world.entrance_shuffle[player]
|
shuffle = world.worlds[player].options.entrance_shuffle
|
||||||
difficulty = world.item_pool[player]
|
difficulty = world.worlds[player].options.item_pool
|
||||||
timer = world.timer[player]
|
timer = world.worlds[player].options.timer
|
||||||
goal = world.goal[player]
|
goal = world.worlds[player].options.goal
|
||||||
mode = world.mode[player]
|
mode = world.worlds[player].options.mode
|
||||||
customitemarray = world.customitemarray
|
customitemarray = world.customitemarray
|
||||||
|
|
||||||
pool = []
|
pool = []
|
||||||
@@ -845,10 +856,10 @@ def make_custom_item_pool(world, player):
|
|||||||
thisbottle = world.random.choice(diff.bottles)
|
thisbottle = world.random.choice(diff.bottles)
|
||||||
pool.append(thisbottle)
|
pool.append(thisbottle)
|
||||||
|
|
||||||
if "triforce" in world.goal[player]:
|
if "triforce" in world.worlds[player].options.goal:
|
||||||
pool.extend(["Triforce Piece"] * world.triforce_pieces_available[player])
|
pool.extend(["Triforce Piece"] * world.worlds[player].options.triforce_pieces_available)
|
||||||
itemtotal += world.triforce_pieces_available[player]
|
itemtotal += world.worlds[player].options.triforce_pieces_available
|
||||||
treasure_hunt_required = world.triforce_pieces_required[player]
|
treasure_hunt_required = world.worlds[player].options.triforce_pieces_required
|
||||||
|
|
||||||
if timer in ['display', 'timed', 'timed_countdown']:
|
if timer in ['display', 'timed', 'timed_countdown']:
|
||||||
clock_mode = 'countdown' if timer == 'timed_countdown' else 'stopwatch'
|
clock_mode = 'countdown' if timer == 'timed_countdown' else 'stopwatch'
|
||||||
@@ -862,7 +873,7 @@ def make_custom_item_pool(world, player):
|
|||||||
itemtotal = itemtotal + 1
|
itemtotal = itemtotal + 1
|
||||||
|
|
||||||
if mode == 'standard':
|
if mode == 'standard':
|
||||||
if world.small_key_shuffle[player] == small_key_shuffle.option_universal:
|
if world.worlds[player].options.small_key_shuffle == small_key_shuffle.option_universal:
|
||||||
key_location = world.random.choice(
|
key_location = world.random.choice(
|
||||||
['Secret Passage', 'Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest',
|
['Secret Passage', 'Hyrule Castle - Boomerang Chest', 'Hyrule Castle - Map Chest',
|
||||||
'Hyrule Castle - Zelda\'s Chest', 'Sewers - Dark Cross'])
|
'Hyrule Castle - Zelda\'s Chest', 'Sewers - Dark Cross'])
|
||||||
@@ -885,9 +896,9 @@ def make_custom_item_pool(world, player):
|
|||||||
pool.extend(['Magic Mirror'] * customitemarray[22])
|
pool.extend(['Magic Mirror'] * customitemarray[22])
|
||||||
pool.extend(['Moon Pearl'] * customitemarray[28])
|
pool.extend(['Moon Pearl'] * customitemarray[28])
|
||||||
|
|
||||||
if world.small_key_shuffle[player] == small_key_shuffle.option_universal:
|
if world.worlds[player].options.small_key_shuffle == small_key_shuffle.option_universal:
|
||||||
itemtotal = itemtotal - 28 # Corrects for small keys not being in item pool in universal Mode
|
itemtotal = itemtotal - 28 # Corrects for small keys not being in item pool in universal Mode
|
||||||
if world.key_drop_shuffle[player]:
|
if world.worlds[player].options.key_drop_shuffle:
|
||||||
itemtotal = itemtotal - (len(key_drop_data) - 1)
|
itemtotal = itemtotal - (len(key_drop_data) - 1)
|
||||||
if itemtotal < total_items_to_place:
|
if itemtotal < total_items_to_place:
|
||||||
pool.extend(['Nothing'] * (total_items_to_place - itemtotal))
|
pool.extend(['Nothing'] * (total_items_to_place - itemtotal))
|
||||||
|
|||||||
@@ -11,11 +11,11 @@ def GetBeemizerItem(world, player: int, item):
|
|||||||
return item
|
return item
|
||||||
|
|
||||||
# first roll - replaceable item should be replaced, within beemizer_total_chance
|
# first roll - replaceable item should be replaced, within beemizer_total_chance
|
||||||
if not world.beemizer_total_chance[player] or world.random.random() > (world.beemizer_total_chance[player] / 100):
|
if not world.worlds[player].options.beemizer_total_chance or world.random.random() > (world.worlds[player].options.beemizer_total_chance / 100):
|
||||||
return item
|
return item
|
||||||
|
|
||||||
# second roll - bee replacement should be trap, within beemizer_trap_chance
|
# second roll - bee replacement should be trap, within beemizer_trap_chance
|
||||||
if not world.beemizer_trap_chance[player] or world.random.random() > (world.beemizer_trap_chance[player] / 100):
|
if not world.worlds[player].options.beemizer_trap_chance or world.random.random() > (world.worlds[player].options.beemizer_trap_chance / 100):
|
||||||
return "Bee" if isinstance(item, str) else world.create_item("Bee", player)
|
return "Bee" if isinstance(item, str) else world.create_item("Bee", player)
|
||||||
else:
|
else:
|
||||||
return "Bee Trap" if isinstance(item, str) else world.create_item("Bee Trap", player)
|
return "Bee Trap" if isinstance(item, str) else world.create_item("Bee Trap", player)
|
||||||
|
|||||||
@@ -156,10 +156,10 @@ class OpenPyramid(Choice):
|
|||||||
|
|
||||||
def to_bool(self, world: MultiWorld, player: int) -> bool:
|
def to_bool(self, world: MultiWorld, player: int) -> bool:
|
||||||
if self.value == self.option_goal:
|
if self.value == self.option_goal:
|
||||||
return world.goal[player].current_key in {'crystals', 'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'ganon_pedestal'}
|
return world.worlds[player].options.goal.current_key in {'crystals', 'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'ganon_pedestal'}
|
||||||
elif self.value == self.option_auto:
|
elif self.value == self.option_auto:
|
||||||
return world.goal[player].current_key in {'crystals', 'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'ganon_pedestal'} \
|
return world.worlds[player].options.goal.current_key in {'crystals', 'ganon_triforce_hunt', 'local_ganon_triforce_hunt', 'ganon_pedestal'} \
|
||||||
and (world.entrance_shuffle[player].current_key in {'vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed'} or not
|
and (world.worlds[player].options.entrance_shuffle.current_key in {'vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed'} or not
|
||||||
world.shuffle_ganon)
|
world.shuffle_ganon)
|
||||||
elif self.value == self.option_open:
|
elif self.value == self.option_open:
|
||||||
return True
|
return True
|
||||||
|
|||||||
@@ -2,8 +2,6 @@
|
|||||||
Helper functions to deliver entrance/exit/region sets to OWG rules.
|
Helper functions to deliver entrance/exit/region sets to OWG rules.
|
||||||
"""
|
"""
|
||||||
|
|
||||||
from BaseClasses import Entrance
|
|
||||||
|
|
||||||
from .StateHelpers import can_lift_heavy_rocks, can_boots_clip_lw, can_boots_clip_dw, can_get_glitched_speed_dw
|
from .StateHelpers import can_lift_heavy_rocks, can_boots_clip_lw, can_boots_clip_dw, can_get_glitched_speed_dw
|
||||||
|
|
||||||
|
|
||||||
@@ -222,14 +220,14 @@ def get_invalid_bunny_revival_dungeons():
|
|||||||
|
|
||||||
def overworld_glitch_connections(world, player):
|
def overworld_glitch_connections(world, player):
|
||||||
# Boots-accessible locations.
|
# Boots-accessible locations.
|
||||||
create_owg_connections(player, world, get_boots_clip_exits_lw(world.mode[player] == 'inverted'))
|
create_owg_connections(player, world, get_boots_clip_exits_lw(world.worlds[player].options.mode == 'inverted'))
|
||||||
create_owg_connections(player, world, get_boots_clip_exits_dw(world.mode[player] == 'inverted', player))
|
create_owg_connections(player, world, get_boots_clip_exits_dw(world.worlds[player].options.mode == 'inverted', player))
|
||||||
|
|
||||||
# Glitched speed drops.
|
# Glitched speed drops.
|
||||||
create_owg_connections(player, world, get_glitched_speed_drops_dw(world.mode[player] == 'inverted'))
|
create_owg_connections(player, world, get_glitched_speed_drops_dw(world.worlds[player].options.mode == 'inverted'))
|
||||||
|
|
||||||
# Mirror clip spots.
|
# Mirror clip spots.
|
||||||
if world.mode[player] != 'inverted':
|
if world.worlds[player].options.mode != 'inverted':
|
||||||
create_owg_connections(player, world, get_mirror_clip_spots_dw())
|
create_owg_connections(player, world, get_mirror_clip_spots_dw())
|
||||||
create_owg_connections(player, world, get_mirror_offset_spots_dw())
|
create_owg_connections(player, world, get_mirror_offset_spots_dw())
|
||||||
else:
|
else:
|
||||||
@@ -239,24 +237,24 @@ def overworld_glitch_connections(world, player):
|
|||||||
def overworld_glitches_rules(world, player):
|
def overworld_glitches_rules(world, player):
|
||||||
|
|
||||||
# Boots-accessible locations.
|
# Boots-accessible locations.
|
||||||
set_owg_connection_rules(player, world, get_boots_clip_exits_lw(world.mode[player] == 'inverted'), lambda state: can_boots_clip_lw(state, player))
|
set_owg_connection_rules(player, world, get_boots_clip_exits_lw(world.worlds[player].options.mode == 'inverted'), lambda state: can_boots_clip_lw(state, player))
|
||||||
set_owg_connection_rules(player, world, get_boots_clip_exits_dw(world.mode[player] == 'inverted', player), lambda state: can_boots_clip_dw(state, player))
|
set_owg_connection_rules(player, world, get_boots_clip_exits_dw(world.worlds[player].options.mode == 'inverted', player), lambda state: can_boots_clip_dw(state, player))
|
||||||
|
|
||||||
# Glitched speed drops.
|
# Glitched speed drops.
|
||||||
set_owg_connection_rules(player, world, get_glitched_speed_drops_dw(world.mode[player] == 'inverted'), lambda state: can_get_glitched_speed_dw(state, player))
|
set_owg_connection_rules(player, world, get_glitched_speed_drops_dw(world.worlds[player].options.mode == 'inverted'), lambda state: can_get_glitched_speed_dw(state, player))
|
||||||
# Dark Death Mountain Ledge Clip Spot also accessible with mirror.
|
# Dark Death Mountain Ledge Clip Spot also accessible with mirror.
|
||||||
if world.mode[player] != 'inverted':
|
if world.worlds[player].options.mode != 'inverted':
|
||||||
add_alternate_rule(world.get_entrance('Dark Death Mountain Ledge Clip Spot', player), lambda state: state.has('Magic Mirror', player))
|
add_alternate_rule(world.get_entrance('Dark Death Mountain Ledge Clip Spot', player), lambda state: state.has('Magic Mirror', player))
|
||||||
|
|
||||||
# Mirror clip spots.
|
# Mirror clip spots.
|
||||||
if world.mode[player] != 'inverted':
|
if world.worlds[player].options.mode != 'inverted':
|
||||||
set_owg_connection_rules(player, world, get_mirror_clip_spots_dw(), lambda state: state.has('Magic Mirror', player))
|
set_owg_connection_rules(player, world, get_mirror_clip_spots_dw(), lambda state: state.has('Magic Mirror', player))
|
||||||
set_owg_connection_rules(player, world, get_mirror_offset_spots_dw(), lambda state: state.has('Magic Mirror', player) and can_boots_clip_lw(state, player))
|
set_owg_connection_rules(player, world, get_mirror_offset_spots_dw(), lambda state: state.has('Magic Mirror', player) and can_boots_clip_lw(state, player))
|
||||||
else:
|
else:
|
||||||
set_owg_connection_rules(player, world, get_mirror_offset_spots_lw(player), lambda state: state.has('Magic Mirror', player) and can_boots_clip_dw(state, player))
|
set_owg_connection_rules(player, world, get_mirror_offset_spots_lw(player), lambda state: state.has('Magic Mirror', player) and can_boots_clip_dw(state, player))
|
||||||
|
|
||||||
# Regions that require the boots and some other stuff.
|
# Regions that require the boots and some other stuff.
|
||||||
if world.mode[player] != 'inverted':
|
if world.worlds[player].options.mode != 'inverted':
|
||||||
world.get_entrance('Turtle Rock Teleporter', player).access_rule = lambda state: (can_boots_clip_lw(state, player) or can_lift_heavy_rocks(state, player)) and state.has('Hammer', player)
|
world.get_entrance('Turtle Rock Teleporter', player).access_rule = lambda state: (can_boots_clip_lw(state, player) or can_lift_heavy_rocks(state, player)) and state.has('Hammer', player)
|
||||||
add_alternate_rule(world.get_entrance('Waterfall of Wishing', player), lambda state: state.has('Moon Pearl', player) or state.has('Pegasus Boots', player))
|
add_alternate_rule(world.get_entrance('Waterfall of Wishing', player), lambda state: state.has('Moon Pearl', player) or state.has('Pegasus Boots', player))
|
||||||
else:
|
else:
|
||||||
@@ -279,18 +277,14 @@ def create_no_logic_connections(player, world, connections):
|
|||||||
for entrance, parent_region, target_region, *rule_override in connections:
|
for entrance, parent_region, target_region, *rule_override in connections:
|
||||||
parent = world.get_region(parent_region, player)
|
parent = world.get_region(parent_region, player)
|
||||||
target = world.get_region(target_region, player)
|
target = world.get_region(target_region, player)
|
||||||
connection = Entrance(player, entrance, parent)
|
parent.connect(target, entrance)
|
||||||
parent.exits.append(connection)
|
|
||||||
connection.connect(target)
|
|
||||||
|
|
||||||
|
|
||||||
def create_owg_connections(player, world, connections):
|
def create_owg_connections(player, world, connections):
|
||||||
for entrance, parent_region, target_region, *rule_override in connections:
|
for entrance, parent_region, target_region, *rule_override in connections:
|
||||||
parent = world.get_region(parent_region, player)
|
parent = world.get_region(parent_region, player)
|
||||||
target = world.get_region(target_region, player)
|
target = world.get_region(target_region, player)
|
||||||
connection = Entrance(player, entrance, parent)
|
parent.connect(target, entrance)
|
||||||
parent.exits.append(connection)
|
|
||||||
connection.connect(target)
|
|
||||||
|
|
||||||
|
|
||||||
def set_owg_connection_rules(player, world, connections, default_rule):
|
def set_owg_connection_rules(player, world, connections, default_rule):
|
||||||
|
|||||||
@@ -1,11 +1,11 @@
|
|||||||
import collections
|
import collections
|
||||||
import typing
|
import typing
|
||||||
|
|
||||||
from BaseClasses import Entrance, MultiWorld
|
from BaseClasses import MultiWorld
|
||||||
from .SubClasses import LTTPRegion, LTTPRegionType
|
from .SubClasses import LTTPEntrance, LTTPRegion, LTTPRegionType
|
||||||
|
|
||||||
|
|
||||||
def is_main_entrance(entrance: Entrance) -> bool:
|
def is_main_entrance(entrance: LTTPEntrance) -> bool:
|
||||||
return entrance.parent_region.type in {LTTPRegionType.DarkWorld, LTTPRegionType.LightWorld} if entrance.parent_region.type else True
|
return entrance.parent_region.type in {LTTPRegionType.DarkWorld, LTTPRegionType.LightWorld} if entrance.parent_region.type else True
|
||||||
|
|
||||||
|
|
||||||
@@ -410,7 +410,7 @@ def _create_region(world: MultiWorld, player: int, name: str, type: LTTPRegionTy
|
|||||||
ret = LTTPRegion(name, type, hint, player, world)
|
ret = LTTPRegion(name, type, hint, player, world)
|
||||||
if exits:
|
if exits:
|
||||||
for exit in exits:
|
for exit in exits:
|
||||||
ret.exits.append(Entrance(player, exit, ret))
|
ret.create_exit(exit)
|
||||||
if locations:
|
if locations:
|
||||||
for location in locations:
|
for location in locations:
|
||||||
if location in key_drop_data:
|
if location in key_drop_data:
|
||||||
|
|||||||
@@ -92,7 +92,7 @@ class LocalRom:
|
|||||||
# cause crash to provide traceback
|
# cause crash to provide traceback
|
||||||
import xxtea
|
import xxtea
|
||||||
|
|
||||||
local_random = world.per_slot_randoms[player]
|
local_random = world.worlds[player].random
|
||||||
key = bytes(local_random.getrandbits(8 * 16).to_bytes(16, 'big'))
|
key = bytes(local_random.getrandbits(8 * 16).to_bytes(16, 'big'))
|
||||||
self.write_bytes(0x1800B0, bytearray(key))
|
self.write_bytes(0x1800B0, bytearray(key))
|
||||||
self.write_int16(0x180087, 1)
|
self.write_int16(0x180087, 1)
|
||||||
@@ -281,7 +281,6 @@ def apply_random_sprite_on_event(rom: LocalRom, sprite, local_random, allow_rand
|
|||||||
|
|
||||||
def patch_enemizer(world, rom: LocalRom, enemizercli, output_directory):
|
def patch_enemizer(world, rom: LocalRom, enemizercli, output_directory):
|
||||||
player = world.player
|
player = world.player
|
||||||
multiworld = world.multiworld
|
|
||||||
check_enemizer(enemizercli)
|
check_enemizer(enemizercli)
|
||||||
randopatch_path = os.path.abspath(os.path.join(output_directory, f'enemizer_randopatch_{player}.sfc'))
|
randopatch_path = os.path.abspath(os.path.join(output_directory, f'enemizer_randopatch_{player}.sfc'))
|
||||||
options_path = os.path.abspath(os.path.join(output_directory, f'enemizer_options_{player}.json'))
|
options_path = os.path.abspath(os.path.join(output_directory, f'enemizer_options_{player}.json'))
|
||||||
@@ -289,18 +288,18 @@ def patch_enemizer(world, rom: LocalRom, enemizercli, output_directory):
|
|||||||
|
|
||||||
# write options file for enemizer
|
# write options file for enemizer
|
||||||
options = {
|
options = {
|
||||||
'RandomizeEnemies': multiworld.enemy_shuffle[player].value,
|
'RandomizeEnemies': world.options.enemy_shuffle.value,
|
||||||
'RandomizeEnemiesType': 3,
|
'RandomizeEnemiesType': 3,
|
||||||
'RandomizeBushEnemyChance': multiworld.bush_shuffle[player].value,
|
'RandomizeBushEnemyChance': world.options.bush_shuffle.value,
|
||||||
'RandomizeEnemyHealthRange': multiworld.enemy_health[player] != 'default',
|
'RandomizeEnemyHealthRange': world.options.enemy_health != 'default',
|
||||||
'RandomizeEnemyHealthType': {'default': 0, 'easy': 0, 'normal': 1, 'hard': 2, 'expert': 3}[
|
'RandomizeEnemyHealthType': {'default': 0, 'easy': 0, 'normal': 1, 'hard': 2, 'expert': 3}[
|
||||||
multiworld.enemy_health[player].current_key],
|
world.options.enemy_health.current_key],
|
||||||
'OHKO': False,
|
'OHKO': False,
|
||||||
'RandomizeEnemyDamage': multiworld.enemy_damage[player] != 'default',
|
'RandomizeEnemyDamage': world.options.enemy_damage != 'default',
|
||||||
'AllowEnemyZeroDamage': True,
|
'AllowEnemyZeroDamage': True,
|
||||||
'ShuffleEnemyDamageGroups': multiworld.enemy_damage[player] != 'default',
|
'ShuffleEnemyDamageGroups': world.options.enemy_damage != 'default',
|
||||||
'EnemyDamageChaosMode': multiworld.enemy_damage[player] == 'chaos',
|
'EnemyDamageChaosMode': world.options.enemy_damage == 'chaos',
|
||||||
'EasyModeEscape': multiworld.mode[player] == "standard",
|
'EasyModeEscape': world.options.mode == "standard",
|
||||||
'EnemiesAbsorbable': False,
|
'EnemiesAbsorbable': False,
|
||||||
'AbsorbableSpawnRate': 10,
|
'AbsorbableSpawnRate': 10,
|
||||||
'AbsorbableTypes': {
|
'AbsorbableTypes': {
|
||||||
@@ -329,7 +328,7 @@ def patch_enemizer(world, rom: LocalRom, enemizercli, output_directory):
|
|||||||
'GrayscaleMode': False,
|
'GrayscaleMode': False,
|
||||||
'GenerateSpoilers': False,
|
'GenerateSpoilers': False,
|
||||||
'RandomizeLinkSpritePalette': False,
|
'RandomizeLinkSpritePalette': False,
|
||||||
'RandomizePots': multiworld.pot_shuffle[player].value,
|
'RandomizePots': world.options.pot_shuffle.value,
|
||||||
'ShuffleMusic': False,
|
'ShuffleMusic': False,
|
||||||
'BootlegMagic': True,
|
'BootlegMagic': True,
|
||||||
'CustomBosses': False,
|
'CustomBosses': False,
|
||||||
@@ -342,7 +341,7 @@ def patch_enemizer(world, rom: LocalRom, enemizercli, output_directory):
|
|||||||
'BeesLevel': 0,
|
'BeesLevel': 0,
|
||||||
'RandomizeTileTrapPattern': False,
|
'RandomizeTileTrapPattern': False,
|
||||||
'RandomizeTileTrapFloorTile': False,
|
'RandomizeTileTrapFloorTile': False,
|
||||||
'AllowKillableThief': multiworld.killable_thieves[player].value,
|
'AllowKillableThief': world.options.killable_thieves.value,
|
||||||
'RandomizeSpriteOnHit': False,
|
'RandomizeSpriteOnHit': False,
|
||||||
'DebugMode': False,
|
'DebugMode': False,
|
||||||
'DebugForceEnemy': False,
|
'DebugForceEnemy': False,
|
||||||
@@ -366,13 +365,13 @@ def patch_enemizer(world, rom: LocalRom, enemizercli, output_directory):
|
|||||||
'MiseryMire': world.dungeons["Misery Mire"].boss.enemizer_name,
|
'MiseryMire': world.dungeons["Misery Mire"].boss.enemizer_name,
|
||||||
'TurtleRock': world.dungeons["Turtle Rock"].boss.enemizer_name,
|
'TurtleRock': world.dungeons["Turtle Rock"].boss.enemizer_name,
|
||||||
'GanonsTower1':
|
'GanonsTower1':
|
||||||
world.dungeons["Ganons Tower" if multiworld.mode[player] != 'inverted' else
|
world.dungeons["Ganons Tower" if world.options.mode != 'inverted' else
|
||||||
"Inverted Ganons Tower"].bosses['bottom'].enemizer_name,
|
"Inverted Ganons Tower"].bosses['bottom'].enemizer_name,
|
||||||
'GanonsTower2':
|
'GanonsTower2':
|
||||||
world.dungeons["Ganons Tower" if multiworld.mode[player] != 'inverted' else
|
world.dungeons["Ganons Tower" if world.options.mode != 'inverted' else
|
||||||
"Inverted Ganons Tower"].bosses['middle'].enemizer_name,
|
"Inverted Ganons Tower"].bosses['middle'].enemizer_name,
|
||||||
'GanonsTower3':
|
'GanonsTower3':
|
||||||
world.dungeons["Ganons Tower" if multiworld.mode[player] != 'inverted' else
|
world.dungeons["Ganons Tower" if world.options.mode != 'inverted' else
|
||||||
"Inverted Ganons Tower"].bosses['top'].enemizer_name,
|
"Inverted Ganons Tower"].bosses['top'].enemizer_name,
|
||||||
'GanonsTower4': 'Agahnim2',
|
'GanonsTower4': 'Agahnim2',
|
||||||
'Ganon': 'Ganon',
|
'Ganon': 'Ganon',
|
||||||
@@ -386,7 +385,7 @@ def patch_enemizer(world, rom: LocalRom, enemizercli, output_directory):
|
|||||||
|
|
||||||
max_enemizer_tries = 5
|
max_enemizer_tries = 5
|
||||||
for i in range(max_enemizer_tries):
|
for i in range(max_enemizer_tries):
|
||||||
enemizer_seed = str(multiworld.per_slot_randoms[player].randint(0, 999999999))
|
enemizer_seed = str(world.random.randint(0, 999999999))
|
||||||
enemizer_command = [os.path.abspath(enemizercli),
|
enemizer_command = [os.path.abspath(enemizercli),
|
||||||
'--rom', randopatch_path,
|
'--rom', randopatch_path,
|
||||||
'--seed', enemizer_seed,
|
'--seed', enemizer_seed,
|
||||||
@@ -416,7 +415,7 @@ def patch_enemizer(world, rom: LocalRom, enemizercli, output_directory):
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
for j in range(i + 1, max_enemizer_tries):
|
for j in range(i + 1, max_enemizer_tries):
|
||||||
multiworld.per_slot_randoms[player].randint(0, 999999999)
|
world.random.randint(0, 999999999)
|
||||||
# Sacrifice all remaining random numbers that would have been used for unused enemizer tries.
|
# Sacrifice all remaining random numbers that would have been used for unused enemizer tries.
|
||||||
# This allows for future enemizer bug fixes to NOT affect the rest of the seed's randomness
|
# This allows for future enemizer bug fixes to NOT affect the rest of the seed's randomness
|
||||||
break
|
break
|
||||||
@@ -430,7 +429,7 @@ def patch_enemizer(world, rom: LocalRom, enemizercli, output_directory):
|
|||||||
|
|
||||||
# Moblins attached to "key drop" locations crash the game when dropping their item when Key Drop Shuffle is on.
|
# Moblins attached to "key drop" locations crash the game when dropping their item when Key Drop Shuffle is on.
|
||||||
# Replace them with a Slime enemy if they are placed.
|
# Replace them with a Slime enemy if they are placed.
|
||||||
if multiworld.key_drop_shuffle[player]:
|
if world.options.key_drop_shuffle:
|
||||||
key_drop_enemies = {
|
key_drop_enemies = {
|
||||||
0x4DA20, 0x4DA5C, 0x4DB7F, 0x4DD73, 0x4DDC3, 0x4DE07, 0x4E201,
|
0x4DA20, 0x4DA5C, 0x4DB7F, 0x4DD73, 0x4DDC3, 0x4DE07, 0x4E201,
|
||||||
0x4E20A, 0x4E326, 0x4E4F7, 0x4E687, 0x4E70C, 0x4E7C8, 0x4E7FA
|
0x4E20A, 0x4E326, 0x4E4F7, 0x4E687, 0x4E70C, 0x4E7C8, 0x4E7FA
|
||||||
@@ -792,8 +791,8 @@ def get_nonnative_item_sprite(code: int) -> int:
|
|||||||
|
|
||||||
|
|
||||||
def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
||||||
|
local_random = world.worlds[player].random
|
||||||
local_world = world.worlds[player]
|
local_world = world.worlds[player]
|
||||||
local_random = local_world.random
|
|
||||||
|
|
||||||
# patch items
|
# patch items
|
||||||
|
|
||||||
@@ -840,20 +839,20 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
|||||||
|
|
||||||
# patch music
|
# patch music
|
||||||
music_addresses = dungeon_music_addresses[location.name]
|
music_addresses = dungeon_music_addresses[location.name]
|
||||||
if world.map_shuffle[player]:
|
if local_world.options.map_shuffle:
|
||||||
music = local_random.choice([0x11, 0x16])
|
music = local_random.choice([0x11, 0x16])
|
||||||
else:
|
else:
|
||||||
music = 0x11 if 'Pendant' in location.item.name else 0x16
|
music = 0x11 if 'Pendant' in location.item.name else 0x16
|
||||||
for music_address in music_addresses:
|
for music_address in music_addresses:
|
||||||
rom.write_byte(music_address, music)
|
rom.write_byte(music_address, music)
|
||||||
|
|
||||||
if world.map_shuffle[player]:
|
if local_world.options.map_shuffle:
|
||||||
rom.write_byte(0x155C9, local_random.choice([0x11, 0x16])) # Randomize GT music too with map shuffle
|
rom.write_byte(0x155C9, local_random.choice([0x11, 0x16])) # Randomize GT music too with map shuffle
|
||||||
|
|
||||||
# patch entrance/exits/holes
|
# patch entrance/exits/holes
|
||||||
for region in world.regions:
|
for region in world.get_regions(player):
|
||||||
for exit in region.exits:
|
for exit in region.exits:
|
||||||
if exit.target is not None and exit.player == player:
|
if exit.target is not None:
|
||||||
if isinstance(exit.addresses, tuple):
|
if isinstance(exit.addresses, tuple):
|
||||||
offset = exit.target
|
offset = exit.target
|
||||||
room_id, ow_area, vram_loc, scroll_y, scroll_x, link_y, link_x, camera_y, camera_x, unknown_1, unknown_2, door_1, door_2 = exit.addresses
|
room_id, ow_area, vram_loc, scroll_y, scroll_x, link_y, link_x, camera_y, camera_x, unknown_1, unknown_2, door_1, door_2 = exit.addresses
|
||||||
@@ -868,15 +867,15 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
|||||||
# Thanks to Zarby89 for originally finding these values
|
# Thanks to Zarby89 for originally finding these values
|
||||||
# todo fix screen scrolling
|
# todo fix screen scrolling
|
||||||
|
|
||||||
if world.entrance_shuffle[player] != 'insanity' and \
|
if local_world.options.entrance_shuffle != 'insanity' and \
|
||||||
exit.name in {'Eastern Palace Exit', 'Tower of Hera Exit', 'Thieves Town Exit',
|
exit.name in {'Eastern Palace Exit', 'Tower of Hera Exit', 'Thieves Town Exit',
|
||||||
'Skull Woods Final Section Exit', 'Ice Palace Exit', 'Misery Mire Exit',
|
'Skull Woods Final Section Exit', 'Ice Palace Exit', 'Misery Mire Exit',
|
||||||
'Palace of Darkness Exit', 'Swamp Palace Exit', 'Ganons Tower Exit',
|
'Palace of Darkness Exit', 'Swamp Palace Exit', 'Ganons Tower Exit',
|
||||||
'Desert Palace Exit (North)', 'Agahnims Tower Exit', 'Spiral Cave Exit (Top)',
|
'Desert Palace Exit (North)', 'Agahnims Tower Exit', 'Spiral Cave Exit (Top)',
|
||||||
'Superbunny Cave Exit (Bottom)', 'Turtle Rock Ledge Exit (East)'} and \
|
'Superbunny Cave Exit (Bottom)', 'Turtle Rock Ledge Exit (East)'} and \
|
||||||
(world.glitches_required[player] not in ['hybrid_major_glitches', 'no_logic'] or
|
(local_world.options.glitches_required not in ['hybrid_major_glitches', 'no_logic'] or
|
||||||
exit.name not in {'Palace of Darkness Exit', 'Tower of Hera Exit', 'Swamp Palace Exit'}):
|
exit.name not in {'Palace of Darkness Exit', 'Tower of Hera Exit', 'Swamp Palace Exit'}):
|
||||||
# For exits that connot be reached from another, no need to apply offset fixes.
|
# For exits that cannot be reached from another, no need to apply offset fixes.
|
||||||
rom.write_int16(0x15DB5 + 2 * offset, link_y) # same as final else
|
rom.write_int16(0x15DB5 + 2 * offset, link_y) # same as final else
|
||||||
elif room_id == 0x0059 and local_world.fix_skullwoods_exit:
|
elif room_id == 0x0059 and local_world.fix_skullwoods_exit:
|
||||||
rom.write_int16(0x15DB5 + 2 * offset, 0x00F8)
|
rom.write_int16(0x15DB5 + 2 * offset, 0x00F8)
|
||||||
@@ -903,7 +902,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
|||||||
else:
|
else:
|
||||||
# patch door table
|
# patch door table
|
||||||
rom.write_byte(0xDBB73 + exit.addresses, exit.target)
|
rom.write_byte(0xDBB73 + exit.addresses, exit.target)
|
||||||
if world.mode[player] == 'inverted':
|
if local_world.options.mode == 'inverted':
|
||||||
patch_shuffled_dark_sanc(world, rom, player)
|
patch_shuffled_dark_sanc(world, rom, player)
|
||||||
|
|
||||||
write_custom_shops(rom, world, player)
|
write_custom_shops(rom, world, player)
|
||||||
@@ -914,16 +913,16 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
|||||||
return 0x53 + int(num), 0x79 + int(num)
|
return 0x53 + int(num), 0x79 + int(num)
|
||||||
|
|
||||||
credits_total = 216
|
credits_total = 216
|
||||||
if world.retro_caves[player]: # Old man cave and Take any caves will count towards collection rate.
|
if local_world.options.retro_caves: # Old man cave and Take any caves will count towards collection rate.
|
||||||
credits_total += 5
|
credits_total += 5
|
||||||
if world.shop_item_slots[player]: # Potion shop only counts towards collection rate if included in the shuffle.
|
if local_world.options.shop_item_slots: # Potion shop only counts towards collection rate if included in the shuffle.
|
||||||
credits_total += 30 if world.include_witch_hut[player] else 27
|
credits_total += 30 if local_world.options.include_witch_hut else 27
|
||||||
if world.shuffle_capacity_upgrades[player]:
|
if local_world.options.shuffle_capacity_upgrades:
|
||||||
credits_total += 2
|
credits_total += 2
|
||||||
|
|
||||||
rom.write_byte(0x187010, credits_total) # dynamic credits
|
rom.write_byte(0x187010, credits_total) # dynamic credits
|
||||||
|
|
||||||
if world.key_drop_shuffle[player]:
|
if local_world.options.key_drop_shuffle:
|
||||||
rom.write_byte(0x140000, 1) # enable key drop shuffle
|
rom.write_byte(0x140000, 1) # enable key drop shuffle
|
||||||
credits_total += len(key_drop_data)
|
credits_total += len(key_drop_data)
|
||||||
# update dungeon counters
|
# update dungeon counters
|
||||||
@@ -977,11 +976,11 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
|||||||
rom.write_byte(0x51DE, 0x00)
|
rom.write_byte(0x51DE, 0x00)
|
||||||
|
|
||||||
# set open mode:
|
# set open mode:
|
||||||
if world.mode[player] in ['open', 'inverted']:
|
if local_world.options.mode in ['open', 'inverted']:
|
||||||
rom.write_byte(0x180032, 0x01) # open mode
|
rom.write_byte(0x180032, 0x01) # open mode
|
||||||
if world.mode[player] == 'inverted':
|
if local_world.options.mode == 'inverted':
|
||||||
set_inverted_mode(world, player, rom)
|
set_inverted_mode(world, player, rom)
|
||||||
elif world.mode[player] == 'standard':
|
elif local_world.options.mode == 'standard':
|
||||||
rom.write_byte(0x180032, 0x00) # standard mode
|
rom.write_byte(0x180032, 0x00) # standard mode
|
||||||
|
|
||||||
uncle_location = world.get_location('Link\'s Uncle', player)
|
uncle_location = world.get_location('Link\'s Uncle', player)
|
||||||
@@ -1001,7 +1000,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
|||||||
rom.write_bytes(0x6D323, [0x00, 0x00, 0xe4, 0xff, 0x08, 0x0E])
|
rom.write_bytes(0x6D323, [0x00, 0x00, 0xe4, 0xff, 0x08, 0x0E])
|
||||||
|
|
||||||
# set light cones
|
# set light cones
|
||||||
rom.write_byte(0x180038, 0x01 if world.mode[player] == "standard" else 0x00)
|
rom.write_byte(0x180038, 0x01 if local_world.options.mode == "standard" else 0x00)
|
||||||
rom.write_byte(0x180039, 0x01 if world.light_world_light_cone else 0x00)
|
rom.write_byte(0x180039, 0x01 if world.light_world_light_cone else 0x00)
|
||||||
rom.write_byte(0x18003A, 0x01 if world.dark_world_light_cone else 0x00)
|
rom.write_byte(0x18003A, 0x01 if world.dark_world_light_cone else 0x00)
|
||||||
|
|
||||||
@@ -1011,7 +1010,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
|||||||
rom.write_byte(0x18004F, 0x01) # Byrna Invulnerability: on
|
rom.write_byte(0x18004F, 0x01) # Byrna Invulnerability: on
|
||||||
|
|
||||||
# handle item_functionality
|
# handle item_functionality
|
||||||
if world.item_functionality[player] == 'hard':
|
if local_world.options.item_functionality == 'hard':
|
||||||
rom.write_byte(0x180181, 0x01) # Make silver arrows work only on ganon
|
rom.write_byte(0x180181, 0x01) # Make silver arrows work only on ganon
|
||||||
rom.write_byte(0x180182, 0x00) # Don't auto equip silvers on pickup
|
rom.write_byte(0x180182, 0x00) # Don't auto equip silvers on pickup
|
||||||
# Powdered Fairies Prize
|
# Powdered Fairies Prize
|
||||||
@@ -1031,7 +1030,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
|||||||
rom.write_int16(0x180036, world.rupoor_cost)
|
rom.write_int16(0x180036, world.rupoor_cost)
|
||||||
# Set stun items
|
# Set stun items
|
||||||
rom.write_byte(0x180180, 0x02) # Hookshot only
|
rom.write_byte(0x180180, 0x02) # Hookshot only
|
||||||
elif world.item_functionality[player] == 'expert':
|
elif local_world.options.item_functionality == 'expert':
|
||||||
rom.write_byte(0x180181, 0x01) # Make silver arrows work only on ganon
|
rom.write_byte(0x180181, 0x01) # Make silver arrows work only on ganon
|
||||||
rom.write_byte(0x180182, 0x00) # Don't auto equip silvers on pickup
|
rom.write_byte(0x180182, 0x00) # Don't auto equip silvers on pickup
|
||||||
# Powdered Fairies Prize
|
# Powdered Fairies Prize
|
||||||
@@ -1071,7 +1070,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
|||||||
# Set stun items
|
# Set stun items
|
||||||
rom.write_byte(0x180180, 0x03) # All standard items
|
rom.write_byte(0x180180, 0x03) # All standard items
|
||||||
# Set overflow items for progressive equipment
|
# Set overflow items for progressive equipment
|
||||||
if world.timer[player] in ['timed', 'timed_countdown', 'timed_ohko']:
|
if local_world.options.timer in ['timed', 'timed_countdown', 'timed_ohko']:
|
||||||
overflow_replacement = GREEN_CLOCK
|
overflow_replacement = GREEN_CLOCK
|
||||||
else:
|
else:
|
||||||
overflow_replacement = GREEN_TWENTY_RUPEES
|
overflow_replacement = GREEN_TWENTY_RUPEES
|
||||||
@@ -1083,7 +1082,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
|||||||
|
|
||||||
# Set overflow items for progressive equipment
|
# Set overflow items for progressive equipment
|
||||||
rom.write_bytes(0x180090,
|
rom.write_bytes(0x180090,
|
||||||
[difficulty.progressive_sword_limit if not world.swordless[player] else 0,
|
[difficulty.progressive_sword_limit if not local_world.options.swordless else 0,
|
||||||
item_table[difficulty.basicsword[-1]].item_code,
|
item_table[difficulty.basicsword[-1]].item_code,
|
||||||
difficulty.progressive_shield_limit, item_table[difficulty.basicshield[-1]].item_code,
|
difficulty.progressive_shield_limit, item_table[difficulty.basicshield[-1]].item_code,
|
||||||
difficulty.progressive_armor_limit, item_table[difficulty.basicarmor[-1]].item_code,
|
difficulty.progressive_armor_limit, item_table[difficulty.basicarmor[-1]].item_code,
|
||||||
@@ -1091,7 +1090,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
|||||||
difficulty.progressive_bow_limit, item_table[difficulty.basicbow[-1]].item_code])
|
difficulty.progressive_bow_limit, item_table[difficulty.basicbow[-1]].item_code])
|
||||||
|
|
||||||
if difficulty.progressive_bow_limit < 2 and (
|
if difficulty.progressive_bow_limit < 2 and (
|
||||||
world.swordless[player] or world.glitches_required[player] == 'no_glitches'):
|
local_world.options.swordless or local_world.options.glitches_required == 'no_glitches'):
|
||||||
rom.write_bytes(0x180098, [2, item_table["Silver Bow"].item_code])
|
rom.write_bytes(0x180098, [2, item_table["Silver Bow"].item_code])
|
||||||
rom.write_byte(0x180181, 0x01) # Make silver arrows work only on ganon
|
rom.write_byte(0x180181, 0x01) # Make silver arrows work only on ganon
|
||||||
rom.write_byte(0x180182, 0x00) # Don't auto equip silvers on pickup
|
rom.write_byte(0x180182, 0x00) # Don't auto equip silvers on pickup
|
||||||
@@ -1099,15 +1098,15 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
|||||||
# set up game internal RNG seed
|
# set up game internal RNG seed
|
||||||
rom.write_bytes(0x178000, local_random.getrandbits(8 * 1024).to_bytes(1024, 'big'))
|
rom.write_bytes(0x178000, local_random.getrandbits(8 * 1024).to_bytes(1024, 'big'))
|
||||||
prize_replacements = {}
|
prize_replacements = {}
|
||||||
if world.item_functionality[player] in ['hard', 'expert']:
|
if local_world.options.item_functionality in ['hard', 'expert']:
|
||||||
prize_replacements[0xE0] = 0xDF # Fairy -> heart
|
prize_replacements[0xE0] = 0xDF # Fairy -> heart
|
||||||
prize_replacements[0xE3] = 0xD8 # Big magic -> small magic
|
prize_replacements[0xE3] = 0xD8 # Big magic -> small magic
|
||||||
|
|
||||||
if world.retro_bow[player]:
|
if local_world.options.retro_bow:
|
||||||
prize_replacements[0xE1] = 0xDA # 5 Arrows -> Blue Rupee
|
prize_replacements[0xE1] = 0xDA # 5 Arrows -> Blue Rupee
|
||||||
prize_replacements[0xE2] = 0xDB # 10 Arrows -> Red Rupee
|
prize_replacements[0xE2] = 0xDB # 10 Arrows -> Red Rupee
|
||||||
|
|
||||||
if world.shuffle_prizes[player] in ("general", "both"):
|
if local_world.options.shuffle_prizes in ("general", "both"):
|
||||||
# shuffle prize packs
|
# shuffle prize packs
|
||||||
prizes = [0xD8, 0xD8, 0xD8, 0xD8, 0xD9, 0xD8, 0xD8, 0xD9, 0xDA, 0xD9, 0xDA, 0xDB, 0xDA, 0xD9, 0xDA, 0xDA, 0xE0,
|
prizes = [0xD8, 0xD8, 0xD8, 0xD8, 0xD9, 0xD8, 0xD8, 0xD9, 0xDA, 0xD9, 0xDA, 0xDB, 0xDA, 0xD9, 0xDA, 0xDA, 0xE0,
|
||||||
0xDF, 0xDF, 0xDA, 0xE0, 0xDF, 0xD8, 0xDF,
|
0xDF, 0xDF, 0xDA, 0xE0, 0xDF, 0xD8, 0xDF,
|
||||||
@@ -1169,7 +1168,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
|||||||
byte = int(rom.read_byte(address))
|
byte = int(rom.read_byte(address))
|
||||||
rom.write_byte(address, prize_replacements.get(byte, byte))
|
rom.write_byte(address, prize_replacements.get(byte, byte))
|
||||||
|
|
||||||
if world.shuffle_prizes[player] in ("bonk", "both"):
|
if local_world.options.shuffle_prizes in ("bonk", "both"):
|
||||||
# set bonk prizes
|
# set bonk prizes
|
||||||
bonk_prizes = [0x79, 0xE3, 0x79, 0xAC, 0xAC, 0xE0, 0xDC, 0xAC, 0xE3, 0xE3, 0xDA, 0xE3, 0xDA, 0xD8, 0xAC,
|
bonk_prizes = [0x79, 0xE3, 0x79, 0xAC, 0xAC, 0xE0, 0xDC, 0xAC, 0xE3, 0xE3, 0xDA, 0xE3, 0xDA, 0xD8, 0xAC,
|
||||||
0xAC, 0xE3, 0xD8, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xDC, 0xDB, 0xE3, 0xDA, 0x79, 0x79,
|
0xAC, 0xE3, 0xD8, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xE3, 0xDC, 0xDB, 0xE3, 0xDA, 0x79, 0x79,
|
||||||
@@ -1196,7 +1195,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
|||||||
0x12, 0x01, 0x35, 0xFF, # lamp -> 5 rupees
|
0x12, 0x01, 0x35, 0xFF, # lamp -> 5 rupees
|
||||||
0x51, 0x06, 0x52, 0xFF, # 6 +5 bomb upgrades -> +10 bomb upgrade
|
0x51, 0x06, 0x52, 0xFF, # 6 +5 bomb upgrades -> +10 bomb upgrade
|
||||||
0x53, 0x06, 0x54, 0xFF, # 6 +5 arrow upgrades -> +10 arrow upgrade
|
0x53, 0x06, 0x54, 0xFF, # 6 +5 arrow upgrades -> +10 arrow upgrade
|
||||||
0x58, 0x01, 0x36 if world.retro_bow[player] else 0x43, 0xFF, # silver arrows -> single arrow (red 20 in retro mode)
|
0x58, 0x01, 0x36 if local_world.options.retro_bow else 0x43, 0xFF, # silver arrows -> single arrow (red 20 in retro mode)
|
||||||
0x3E, difficulty.boss_heart_container_limit, 0x47, 0xff, # boss heart -> green 20
|
0x3E, difficulty.boss_heart_container_limit, 0x47, 0xff, # boss heart -> green 20
|
||||||
0x17, difficulty.heart_piece_limit, 0x47, 0xff, # piece of heart -> green 20
|
0x17, difficulty.heart_piece_limit, 0x47, 0xff, # piece of heart -> green 20
|
||||||
0xFF, 0xFF, 0xFF, 0xFF, # end of table sentinel
|
0xFF, 0xFF, 0xFF, 0xFF, # end of table sentinel
|
||||||
@@ -1238,13 +1237,13 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
|||||||
rom.write_byte(0x180029, 0x01) # Smithy quick item give
|
rom.write_byte(0x180029, 0x01) # Smithy quick item give
|
||||||
|
|
||||||
# set swordless mode settings
|
# set swordless mode settings
|
||||||
rom.write_byte(0x18003F, 0x01 if world.swordless[player] else 0x00) # hammer can harm ganon
|
rom.write_byte(0x18003F, 0x01 if local_world.options.swordless else 0x00) # hammer can harm ganon
|
||||||
rom.write_byte(0x180040, 0x01 if world.swordless[player] else 0x00) # open curtains
|
rom.write_byte(0x180040, 0x01 if local_world.options.swordless else 0x00) # open curtains
|
||||||
rom.write_byte(0x180041, 0x01 if world.swordless[player] else 0x00) # swordless medallions
|
rom.write_byte(0x180041, 0x01 if local_world.options.swordless else 0x00) # swordless medallions
|
||||||
rom.write_byte(0x180043, 0xFF if world.swordless[player] else 0x00) # starting sword for link
|
rom.write_byte(0x180043, 0xFF if local_world.options.swordless else 0x00) # starting sword for link
|
||||||
rom.write_byte(0x180044, 0x01 if world.swordless[player] else 0x00) # hammer activates tablets
|
rom.write_byte(0x180044, 0x01 if local_world.options.swordless else 0x00) # hammer activates tablets
|
||||||
|
|
||||||
if world.item_functionality[player] == 'easy':
|
if local_world.options.item_functionality == 'easy':
|
||||||
rom.write_byte(0x18003F, 0x01) # hammer can harm ganon
|
rom.write_byte(0x18003F, 0x01) # hammer can harm ganon
|
||||||
rom.write_byte(0x180041, 0x02) # Allow swordless medallion use EVERYWHERE.
|
rom.write_byte(0x180041, 0x02) # Allow swordless medallion use EVERYWHERE.
|
||||||
rom.write_byte(0x180044, 0x01) # hammer activates tablets
|
rom.write_byte(0x180044, 0x01) # hammer activates tablets
|
||||||
@@ -1262,11 +1261,11 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
|||||||
# Set up requested clock settings
|
# Set up requested clock settings
|
||||||
if local_world.clock_mode in ['countdown-ohko', 'stopwatch', 'countdown']:
|
if local_world.clock_mode in ['countdown-ohko', 'stopwatch', 'countdown']:
|
||||||
rom.write_int32(0x180200,
|
rom.write_int32(0x180200,
|
||||||
world.red_clock_time[player] * 60 * 60) # red clock adjustment time (in frames, sint32)
|
local_world.options.red_clock_time * 60 * 60) # red clock adjustment time (in frames, sint32)
|
||||||
rom.write_int32(0x180204,
|
rom.write_int32(0x180204,
|
||||||
world.blue_clock_time[player] * 60 * 60) # blue clock adjustment time (in frames, sint32)
|
local_world.options.blue_clock_time * 60 * 60) # blue clock adjustment time (in frames, sint32)
|
||||||
rom.write_int32(0x180208,
|
rom.write_int32(0x180208,
|
||||||
world.green_clock_time[player] * 60 * 60) # green clock adjustment time (in frames, sint32)
|
local_world.options.green_clock_time * 60 * 60) # green clock adjustment time (in frames, sint32)
|
||||||
else:
|
else:
|
||||||
rom.write_int32(0x180200, 0) # red clock adjustment time (in frames, sint32)
|
rom.write_int32(0x180200, 0) # red clock adjustment time (in frames, sint32)
|
||||||
rom.write_int32(0x180204, 0) # blue clock adjustment time (in frames, sint32)
|
rom.write_int32(0x180204, 0) # blue clock adjustment time (in frames, sint32)
|
||||||
@@ -1274,20 +1273,20 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
|||||||
|
|
||||||
# Set up requested start time for countdown modes
|
# Set up requested start time for countdown modes
|
||||||
if local_world.clock_mode in ['countdown-ohko', 'countdown']:
|
if local_world.clock_mode in ['countdown-ohko', 'countdown']:
|
||||||
rom.write_int32(0x18020C, world.countdown_start_time[player] * 60 * 60) # starting time (in frames, sint32)
|
rom.write_int32(0x18020C, local_world.options.countdown_start_time * 60 * 60) # starting time (in frames, sint32)
|
||||||
else:
|
else:
|
||||||
rom.write_int32(0x18020C, 0) # starting time (in frames, sint32)
|
rom.write_int32(0x18020C, 0) # starting time (in frames, sint32)
|
||||||
|
|
||||||
# set up goals for treasure hunt
|
# set up goals for treasure hunt
|
||||||
rom.write_int16(0x180163, max(0, local_world.treasure_hunt_required -
|
rom.write_int16(0x180163, max(0, local_world.treasure_hunt_required -
|
||||||
sum(1 for item in world.precollected_items[player] if item.name == "Triforce Piece")))
|
sum(1 for item in world.precollected_items[player] if item.name == "Triforce Piece")))
|
||||||
rom.write_bytes(0x180165, [0x0E, 0x28]) # Triforce Piece Sprite
|
rom.write_bytes(0x180165, [0x0E, 0x28]) # Triforce Piece Sprite
|
||||||
rom.write_byte(0x180194, 1) # Must turn in triforced pieces (instant win not enabled)
|
rom.write_byte(0x180194, 1) # Must turn in triforced pieces (instant win not enabled)
|
||||||
|
|
||||||
rom.write_bytes(0x180213, [0x00, 0x01]) # Not a Tournament Seed
|
rom.write_bytes(0x180213, [0x00, 0x01]) # Not a Tournament Seed
|
||||||
|
|
||||||
gametype = 0x04 # item
|
gametype = 0x04 # item
|
||||||
if world.entrance_shuffle[player] != 'vanilla':
|
if local_world.options.entrance_shuffle != 'vanilla':
|
||||||
gametype |= 0x02 # entrance
|
gametype |= 0x02 # entrance
|
||||||
if enemized:
|
if enemized:
|
||||||
gametype |= 0x01 # enemizer
|
gametype |= 0x01 # enemizer
|
||||||
@@ -1298,7 +1297,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
|||||||
rom.write_byte(0x1800A2, 0x01 if local_world.fix_fake_world else 0x00)
|
rom.write_byte(0x1800A2, 0x01 if local_world.fix_fake_world else 0x00)
|
||||||
# Lock or unlock aga tower door during escape sequence.
|
# Lock or unlock aga tower door during escape sequence.
|
||||||
rom.write_byte(0x180169, 0x00)
|
rom.write_byte(0x180169, 0x00)
|
||||||
if world.mode[player] == 'inverted':
|
if local_world.options.mode == 'inverted':
|
||||||
rom.write_byte(0x180169, 0x02) # lock aga/ganon tower door with crystals in inverted
|
rom.write_byte(0x180169, 0x02) # lock aga/ganon tower door with crystals in inverted
|
||||||
rom.write_byte(0x180171,
|
rom.write_byte(0x180171,
|
||||||
0x01 if local_world.ganon_at_pyramid else 0x00) # Enable respawning on pyramid after ganon death
|
0x01 if local_world.ganon_at_pyramid else 0x00) # Enable respawning on pyramid after ganon death
|
||||||
@@ -1309,9 +1308,8 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
|||||||
rom.write_bytes(0x50563, [0x3F, 0x14]) # disable below ganon chest
|
rom.write_bytes(0x50563, [0x3F, 0x14]) # disable below ganon chest
|
||||||
rom.write_byte(0x50599, 0x00) # disable below ganon chest
|
rom.write_byte(0x50599, 0x00) # disable below ganon chest
|
||||||
rom.write_bytes(0xE9A5, [0x7E, 0x00, 0x24]) # disable below ganon chest
|
rom.write_bytes(0xE9A5, [0x7E, 0x00, 0x24]) # disable below ganon chest
|
||||||
rom.write_byte(0x18008B, 0x01 if world.open_pyramid[player].to_bool(world, player) else 0x00) # pre-open Pyramid Hole
|
rom.write_byte(0x18008B, 0x01 if local_world.options.open_pyramid.to_bool(world, player) else 0x00) # pre-open Pyramid Hole
|
||||||
rom.write_byte(0x18008C, 0x01 if world.crystals_needed_for_gt[
|
rom.write_byte(0x18008C, 0x01 if local_world.options.crystals_needed_for_gt == 0 else 0x00) # GT pre-opened if crystal requirement is 0
|
||||||
player] == 0 else 0x00) # GT pre-opened if crystal requirement is 0
|
|
||||||
rom.write_byte(0xF5D73, 0xF0) # bees are catchable
|
rom.write_byte(0xF5D73, 0xF0) # bees are catchable
|
||||||
rom.write_byte(0xF5F10, 0xF0) # bees are catchable
|
rom.write_byte(0xF5F10, 0xF0) # bees are catchable
|
||||||
rom.write_byte(0x180086, 0x00 if world.aga_randomness else 0x01) # set blue ball and ganon warp randomness
|
rom.write_byte(0x180086, 0x00 if world.aga_randomness else 0x01) # set blue ball and ganon warp randomness
|
||||||
@@ -1325,7 +1323,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
|||||||
equip[0x36C] = 0x18
|
equip[0x36C] = 0x18
|
||||||
equip[0x36D] = 0x18
|
equip[0x36D] = 0x18
|
||||||
equip[0x379] = 0x68
|
equip[0x379] = 0x68
|
||||||
starting_max_bombs = 0 if world.bombless_start[player] else 10
|
starting_max_bombs = 0 if local_world.options.bombless_start else 10
|
||||||
starting_max_arrows = 30
|
starting_max_arrows = 30
|
||||||
|
|
||||||
startingstate = CollectionState(world)
|
startingstate = CollectionState(world)
|
||||||
@@ -1333,12 +1331,12 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
|||||||
if startingstate.has('Silver Bow', player):
|
if startingstate.has('Silver Bow', player):
|
||||||
equip[0x340] = 1
|
equip[0x340] = 1
|
||||||
equip[0x38E] |= 0x60
|
equip[0x38E] |= 0x60
|
||||||
if not world.retro_bow[player]:
|
if not local_world.options.retro_bow:
|
||||||
equip[0x38E] |= 0x80
|
equip[0x38E] |= 0x80
|
||||||
elif startingstate.has('Bow', player):
|
elif startingstate.has('Bow', player):
|
||||||
equip[0x340] = 1
|
equip[0x340] = 1
|
||||||
equip[0x38E] |= 0x20 # progressive flag to get the correct hint in all cases
|
equip[0x38E] |= 0x20 # progressive flag to get the correct hint in all cases
|
||||||
if not world.retro_bow[player]:
|
if not local_world.options.retro_bow:
|
||||||
equip[0x38E] |= 0x80
|
equip[0x38E] |= 0x80
|
||||||
if startingstate.has('Silver Arrows', player):
|
if startingstate.has('Silver Arrows', player):
|
||||||
equip[0x38E] |= 0x40
|
equip[0x38E] |= 0x40
|
||||||
@@ -1476,7 +1474,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
|||||||
elif item.name in bombs:
|
elif item.name in bombs:
|
||||||
equip[0x343] += bombs[item.name]
|
equip[0x343] += bombs[item.name]
|
||||||
elif item.name in arrows:
|
elif item.name in arrows:
|
||||||
if world.retro_bow[player]:
|
if local_world.options.retro_bow:
|
||||||
equip[0x38E] |= 0x80
|
equip[0x38E] |= 0x80
|
||||||
equip[0x377] = 1
|
equip[0x377] = 1
|
||||||
else:
|
else:
|
||||||
@@ -1502,16 +1500,13 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
|||||||
rom.write_bytes(0x183000, equip[0x340:])
|
rom.write_bytes(0x183000, equip[0x340:])
|
||||||
rom.write_bytes(0x271A6, equip[0x340:0x340 + 60])
|
rom.write_bytes(0x271A6, equip[0x340:0x340 + 60])
|
||||||
|
|
||||||
rom.write_byte(0x18004A, 0x00 if world.mode[player] != 'inverted' else 0x01) # Inverted mode
|
rom.write_byte(0x18004A, 0x00 if local_world.options.mode != 'inverted' else 0x01) # Inverted mode
|
||||||
rom.write_byte(0x18005D, 0x00) # Hammer always breaks barrier
|
rom.write_byte(0x18005D, 0x00) # Hammer always breaks barrier
|
||||||
rom.write_byte(0x2AF79, 0xD0 if world.mode[
|
rom.write_byte(0x2AF79, 0xD0 if local_world.options.mode != 'inverted' else 0xF0) # vortexes: Normal (D0=light to dark, F0=dark to light, 42 = both)
|
||||||
player] != 'inverted' else 0xF0) # vortexes: Normal (D0=light to dark, F0=dark to light, 42 = both)
|
rom.write_byte(0x3A943, 0xD0 if local_world.options.mode != 'inverted' else 0xF0) # Mirror: Normal (D0=Dark to Light, F0=light to dark, 42 = both)
|
||||||
rom.write_byte(0x3A943, 0xD0 if world.mode[
|
rom.write_byte(0x3A96D, 0xF0 if local_world.options.mode != 'inverted' else 0xD0) # Residual Portal: Normal (F0= Light Side, D0=Dark Side, 42 = both (Darth Vader))
|
||||||
player] != 'inverted' else 0xF0) # Mirror: Normal (D0=Dark to Light, F0=light to dark, 42 = both)
|
|
||||||
rom.write_byte(0x3A96D, 0xF0 if world.mode[
|
|
||||||
player] != 'inverted' else 0xD0) # Residual Portal: Normal (F0= Light Side, D0=Dark Side, 42 = both (Darth Vader))
|
|
||||||
rom.write_byte(0x3A9A7, 0xD0) # Residual Portal: Normal (D0= Light Side, F0=Dark Side, 42 = both (Darth Vader))
|
rom.write_byte(0x3A9A7, 0xD0) # Residual Portal: Normal (D0= Light Side, F0=Dark Side, 42 = both (Darth Vader))
|
||||||
if world.shuffle_capacity_upgrades[player]:
|
if local_world.options.shuffle_capacity_upgrades:
|
||||||
rom.write_bytes(0x180080,
|
rom.write_bytes(0x180080,
|
||||||
[5, 10, 5, 10]) # values to fill for Capacity Upgrades (Bomb5, Bomb10, Arrow5, Arrow10)
|
[5, 10, 5, 10]) # values to fill for Capacity Upgrades (Bomb5, Bomb10, Arrow5, Arrow10)
|
||||||
else:
|
else:
|
||||||
@@ -1522,21 +1517,21 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
|||||||
(0x02 if 'bombs' in local_world.escape_assist else 0x00) |
|
(0x02 if 'bombs' in local_world.escape_assist else 0x00) |
|
||||||
(0x04 if 'magic' in local_world.escape_assist else 0x00))) # Escape assist
|
(0x04 if 'magic' in local_world.escape_assist else 0x00))) # Escape assist
|
||||||
|
|
||||||
if world.goal[player] in ['pedestal', 'triforce_hunt', 'local_triforce_hunt']:
|
if local_world.options.goal in ['pedestal', 'triforce_hunt', 'local_triforce_hunt']:
|
||||||
rom.write_byte(0x18003E, 0x01) # make ganon invincible
|
rom.write_byte(0x18003E, 0x01) # make ganon invincible
|
||||||
elif world.goal[player] in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']:
|
elif local_world.options.goal in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']:
|
||||||
rom.write_byte(0x18003E, 0x05) # make ganon invincible until enough triforce pieces are collected
|
rom.write_byte(0x18003E, 0x05) # make ganon invincible until enough triforce pieces are collected
|
||||||
elif world.goal[player] in ['ganon_pedestal']:
|
elif local_world.options.goal in ['ganon_pedestal']:
|
||||||
rom.write_byte(0x18003E, 0x06)
|
rom.write_byte(0x18003E, 0x06)
|
||||||
elif world.goal[player] in ['bosses']:
|
elif local_world.options.goal in ['bosses']:
|
||||||
rom.write_byte(0x18003E, 0x02) # make ganon invincible until all bosses are beat
|
rom.write_byte(0x18003E, 0x02) # make ganon invincible until all bosses are beat
|
||||||
elif world.goal[player] in ['crystals']:
|
elif local_world.options.goal in ['crystals']:
|
||||||
rom.write_byte(0x18003E, 0x04) # make ganon invincible until all crystals
|
rom.write_byte(0x18003E, 0x04) # make ganon invincible until all crystals
|
||||||
else:
|
else:
|
||||||
rom.write_byte(0x18003E, 0x03) # make ganon invincible until all crystals and aga 2 are collected
|
rom.write_byte(0x18003E, 0x03) # make ganon invincible until all crystals and aga 2 are collected
|
||||||
|
|
||||||
rom.write_byte(0x18005E, world.crystals_needed_for_gt[player])
|
rom.write_byte(0x18005E, local_world.options.crystals_needed_for_gt)
|
||||||
rom.write_byte(0x18005F, world.crystals_needed_for_ganon[player])
|
rom.write_byte(0x18005F, local_world.options.crystals_needed_for_ganon)
|
||||||
|
|
||||||
# Bitfield - enable text box to show with free roaming items
|
# Bitfield - enable text box to show with free roaming items
|
||||||
#
|
#
|
||||||
@@ -1547,21 +1542,20 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
|||||||
# c - enabled for inside compasses
|
# c - enabled for inside compasses
|
||||||
# s - enabled for inside small keys
|
# s - enabled for inside small keys
|
||||||
# block HC upstairs doors in rain state in standard mode
|
# block HC upstairs doors in rain state in standard mode
|
||||||
rom.write_byte(0x18008A, 0x01 if world.mode[player] == "standard" and world.entrance_shuffle[player] != 'vanilla' else 0x00)
|
rom.write_byte(0x18008A, 0x01 if local_world.options.mode == "standard" and local_world.options.entrance_shuffle != 'vanilla' else 0x00)
|
||||||
|
|
||||||
rom.write_byte(0x18016A, 0x10 | ((0x01 if world.small_key_shuffle[player] else 0x00)
|
rom.write_byte(0x18016A, 0x10 | ((0x01 if local_world.options.small_key_shuffle else 0x00)
|
||||||
| (0x02 if world.compass_shuffle[player] else 0x00)
|
| (0x02 if local_world.options.compass_shuffle else 0x00)
|
||||||
| (0x04 if world.map_shuffle[player] else 0x00)
|
| (0x04 if local_world.options.map_shuffle else 0x00)
|
||||||
| (0x08 if world.big_key_shuffle[
|
| (0x08 if local_world.options.big_key_shuffle else 0x00))) # free roaming item text boxes
|
||||||
player] else 0x00))) # free roaming item text boxes
|
rom.write_byte(0x18003B, 0x01 if local_world.options.map_shuffle else 0x00) # maps showing crystals on overworld
|
||||||
rom.write_byte(0x18003B, 0x01 if world.map_shuffle[player] else 0x00) # maps showing crystals on overworld
|
|
||||||
|
|
||||||
# compasses showing dungeon count
|
# compasses showing dungeon count
|
||||||
if local_world.clock_mode or world.dungeon_counters[player] == 'off':
|
if local_world.clock_mode or local_world.options.dungeon_counters == 'off':
|
||||||
rom.write_byte(0x18003C, 0x00) # Currently must be off if timer is on, because they use same HUD location
|
rom.write_byte(0x18003C, 0x00) # Currently must be off if timer is on, because they use same HUD location
|
||||||
elif world.dungeon_counters[player] == 'on':
|
elif local_world.options.dungeon_counters == 'on':
|
||||||
rom.write_byte(0x18003C, 0x02) # always on
|
rom.write_byte(0x18003C, 0x02) # always on
|
||||||
elif world.compass_shuffle[player] or world.dungeon_counters[player] == 'pickup':
|
elif local_world.options.compass_shuffle or local_world.options.dungeon_counters == 'pickup':
|
||||||
rom.write_byte(0x18003C, 0x01) # show on pickup
|
rom.write_byte(0x18003C, 0x01) # show on pickup
|
||||||
else:
|
else:
|
||||||
rom.write_byte(0x18003C, 0x00)
|
rom.write_byte(0x18003C, 0x00)
|
||||||
@@ -1574,11 +1568,11 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
|||||||
# b - Big Key
|
# b - Big Key
|
||||||
# a - Small Key
|
# a - Small Key
|
||||||
#
|
#
|
||||||
rom.write_byte(0x180045, ((0x00 if (world.small_key_shuffle[player] == small_key_shuffle.option_original_dungeon or
|
rom.write_byte(0x180045, ((0x00 if (local_world.options.small_key_shuffle == small_key_shuffle.option_original_dungeon or
|
||||||
world.small_key_shuffle[player] == small_key_shuffle.option_universal) else 0x01)
|
local_world.options.small_key_shuffle == small_key_shuffle.option_universal) else 0x01)
|
||||||
| (0x02 if world.big_key_shuffle[player] else 0x00)
|
| (0x02 if local_world.options.big_key_shuffle else 0x00)
|
||||||
| (0x04 if world.map_shuffle[player] else 0x00)
|
| (0x04 if local_world.options.map_shuffle else 0x00)
|
||||||
| (0x08 if world.compass_shuffle[player] else 0x00))) # free roaming items in menu
|
| (0x08 if local_world.options.compass_shuffle else 0x00))) # free roaming items in menu
|
||||||
|
|
||||||
# Map reveals
|
# Map reveals
|
||||||
reveal_bytes = {
|
reveal_bytes = {
|
||||||
@@ -1604,31 +1598,25 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
|||||||
return 0x0000
|
return 0x0000
|
||||||
|
|
||||||
rom.write_int16(0x18017A,
|
rom.write_int16(0x18017A,
|
||||||
get_reveal_bytes('Green Pendant') if world.map_shuffle[player] else 0x0000) # Sahasrahla reveal
|
get_reveal_bytes('Green Pendant') if local_world.options.map_shuffle else 0x0000) # Sahasrahla reveal
|
||||||
rom.write_int16(0x18017C, get_reveal_bytes('Crystal 5') | get_reveal_bytes('Crystal 6') if world.map_shuffle[
|
rom.write_int16(0x18017C, get_reveal_bytes('Crystal 5') | get_reveal_bytes('Crystal 6') if local_world.options.map_shuffle else 0x0000) # Bomb Shop Reveal
|
||||||
player] else 0x0000) # Bomb Shop Reveal
|
|
||||||
|
|
||||||
rom.write_byte(0x180172, 0x01 if world.small_key_shuffle[
|
rom.write_byte(0x180172, 0x01 if local_world.options.small_key_shuffle == small_key_shuffle.option_universal else 0x00) # universal keys
|
||||||
player] == small_key_shuffle.option_universal else 0x00) # universal keys
|
rom.write_byte(0x18637E, 0x01 if local_world.options.retro_bow else 0x00) # Skip quiver in item shops once bought
|
||||||
rom.write_byte(0x18637E, 0x01 if world.retro_bow[player] else 0x00) # Skip quiver in item shops once bought
|
rom.write_byte(0x180175, 0x01 if local_world.options.retro_bow else 0x00) # rupee bow
|
||||||
rom.write_byte(0x180175, 0x01 if world.retro_bow[player] else 0x00) # rupee bow
|
rom.write_byte(0x180176, 0x0A if local_world.options.retro_bow else 0x00) # wood arrow cost
|
||||||
rom.write_byte(0x180176, 0x0A if world.retro_bow[player] else 0x00) # wood arrow cost
|
rom.write_byte(0x180178, 0x32 if local_world.options.retro_bow else 0x00) # silver arrow cost
|
||||||
rom.write_byte(0x180178, 0x32 if world.retro_bow[player] else 0x00) # silver arrow cost
|
rom.write_byte(0x301FC, 0xDA if local_world.options.retro_bow else 0xE1) # rupees replace arrows under pots
|
||||||
rom.write_byte(0x301FC, 0xDA if world.retro_bow[player] else 0xE1) # rupees replace arrows under pots
|
rom.write_byte(0x30052, 0xDB if local_world.options.retro_bow else 0xE2) # replace arrows in fish prize from bottle merchant
|
||||||
rom.write_byte(0x30052, 0xDB if world.retro_bow[player] else 0xE2) # replace arrows in fish prize from bottle merchant
|
rom.write_bytes(0xECB4E, [0xA9, 0x00, 0xEA, 0xEA] if local_world.options.retro_bow else [0xAF, 0x77, 0xF3, 0x7E]) # Thief steals rupees instead of arrows
|
||||||
rom.write_bytes(0xECB4E, [0xA9, 0x00, 0xEA, 0xEA] if world.retro_bow[player] else [0xAF, 0x77, 0xF3,
|
rom.write_bytes(0xF0D96, [0xA9, 0x00, 0xEA, 0xEA] if local_world.options.retro_bow else [0xAF, 0x77, 0xF3, 0x7E]) # Pikit steals rupees instead of arrows
|
||||||
0x7E]) # Thief steals rupees instead of arrows
|
rom.write_bytes(0xEDA5, [0x35, 0x41] if local_world.options.retro_bow else [0x43, 0x44]) # Chest game gives rupees instead of arrows
|
||||||
rom.write_bytes(0xF0D96, [0xA9, 0x00, 0xEA, 0xEA] if world.retro_bow[player] else [0xAF, 0x77, 0xF3,
|
|
||||||
0x7E]) # Pikit steals rupees instead of arrows
|
|
||||||
rom.write_bytes(0xEDA5,
|
|
||||||
[0x35, 0x41] if world.retro_bow[player] else [0x43, 0x44]) # Chest game gives rupees instead of arrows
|
|
||||||
digging_game_rng = local_random.randint(1, 30) # set rng for digging game
|
digging_game_rng = local_random.randint(1, 30) # set rng for digging game
|
||||||
rom.write_byte(0x180020, digging_game_rng)
|
rom.write_byte(0x180020, digging_game_rng)
|
||||||
rom.write_byte(0xEFD95, digging_game_rng)
|
rom.write_byte(0xEFD95, digging_game_rng)
|
||||||
rom.write_byte(0x1800A3, 0x01) # enable correct world setting behaviour after agahnim kills
|
rom.write_byte(0x1800A3, 0x01) # enable correct world setting behaviour after agahnim kills
|
||||||
rom.write_byte(0x1800A4, 0x01 if world.glitches_required[player] != 'no_logic' else 0x00) # enable POD EG fix
|
rom.write_byte(0x1800A4, 0x01 if local_world.options.glitches_required != 'no_logic' else 0x00) # enable POD EG fix
|
||||||
rom.write_byte(0x186383, 0x01 if world.glitches_required[
|
rom.write_byte(0x186383, 0x01 if local_world.options.glitches_required == 'no_logic' else 0x00) # disable glitching to Triforce from Ganons Room
|
||||||
player] == 'no_logic' else 0x00) # disable glitching to Triforce from Ganons Room
|
|
||||||
rom.write_byte(0x180042, 0x01 if world.save_and_quit_from_boss else 0x00) # Allow Save and Quit after boss kill
|
rom.write_byte(0x180042, 0x01 if world.save_and_quit_from_boss else 0x00) # Allow Save and Quit after boss kill
|
||||||
|
|
||||||
# remove shield from uncle
|
# remove shield from uncle
|
||||||
@@ -1645,7 +1633,7 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
|||||||
rom.write_bytes(0x180185, [0, 0, 0]) # Uncle respawn refills (magic, bombs, arrows)
|
rom.write_bytes(0x180185, [0, 0, 0]) # Uncle respawn refills (magic, bombs, arrows)
|
||||||
rom.write_bytes(0x180188, [0, 0, 0]) # Zelda respawn refills (magic, bombs, arrows)
|
rom.write_bytes(0x180188, [0, 0, 0]) # Zelda respawn refills (magic, bombs, arrows)
|
||||||
rom.write_bytes(0x18018B, [0, 0, 0]) # Mantle respawn refills (magic, bombs, arrows)
|
rom.write_bytes(0x18018B, [0, 0, 0]) # Mantle respawn refills (magic, bombs, arrows)
|
||||||
if world.mode[player] == 'standard' and uncle_location.item and uncle_location.item.player == player:
|
if local_world.options.mode == 'standard' and uncle_location.item and uncle_location.item.player == player:
|
||||||
if uncle_location.item.name in {'Bow', 'Progressive Bow'}:
|
if uncle_location.item.name in {'Bow', 'Progressive Bow'}:
|
||||||
rom.write_byte(0x18004E, 1) # Escape Fill (arrows)
|
rom.write_byte(0x18004E, 1) # Escape Fill (arrows)
|
||||||
rom.write_int16(0x180183, 300) # Escape fill rupee bow
|
rom.write_int16(0x180183, 300) # Escape fill rupee bow
|
||||||
@@ -1673,8 +1661,8 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
|||||||
0xAD, 0xBF, 0x0A, 0xF0, 0x4F])
|
0xAD, 0xBF, 0x0A, 0xF0, 0x4F])
|
||||||
|
|
||||||
# allow smith into multi-entrance caves in appropriate shuffles
|
# allow smith into multi-entrance caves in appropriate shuffles
|
||||||
if world.entrance_shuffle[player] in ['restricted', 'full', 'crossed', 'insanity'] or (
|
if local_world.options.entrance_shuffle in ['restricted', 'full', 'crossed', 'insanity'] or (
|
||||||
world.entrance_shuffle[player] == 'simple' and world.mode[player] == 'inverted'):
|
local_world.options.entrance_shuffle == 'simple' and local_world.options.mode == 'inverted'):
|
||||||
rom.write_byte(0x18004C, 0x01)
|
rom.write_byte(0x18004C, 0x01)
|
||||||
|
|
||||||
# set correct flag for hera basement item
|
# set correct flag for hera basement item
|
||||||
@@ -1694,8 +1682,8 @@ def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
|||||||
rom.write_byte(0xFED31, 0x2A) # bombable exit
|
rom.write_byte(0xFED31, 0x2A) # bombable exit
|
||||||
rom.write_byte(0xFEE41, 0x2A) # bombable exit
|
rom.write_byte(0xFEE41, 0x2A) # bombable exit
|
||||||
|
|
||||||
if world.tile_shuffle[player]:
|
if local_world.options.tile_shuffle:
|
||||||
tile_set = TileSet.get_random_tile_set(world.per_slot_randoms[player])
|
tile_set = TileSet.get_random_tile_set(world.worlds[player].random)
|
||||||
rom.write_byte(0x4BA21, tile_set.get_speed())
|
rom.write_byte(0x4BA21, tile_set.get_speed())
|
||||||
rom.write_byte(0x4BA1D, tile_set.get_len())
|
rom.write_byte(0x4BA1D, tile_set.get_len())
|
||||||
rom.write_bytes(0x4BA2A, tile_set.get_bytes())
|
rom.write_bytes(0x4BA2A, tile_set.get_bytes())
|
||||||
@@ -1770,9 +1758,9 @@ def write_custom_shops(rom, world, player):
|
|||||||
slot = 0 if shop.type == ShopType.TakeAny else index
|
slot = 0 if shop.type == ShopType.TakeAny else index
|
||||||
if item is None:
|
if item is None:
|
||||||
break
|
break
|
||||||
if world.shop_item_slots[player] or shop.type == ShopType.TakeAny:
|
if world.worlds[player].options.shop_item_slots or shop.type == ShopType.TakeAny:
|
||||||
count_shop = (shop.region.name != 'Potion Shop' or world.include_witch_hut[player]) and \
|
count_shop = (shop.region.name != 'Potion Shop' or world.worlds[player].options.include_witch_hut) and \
|
||||||
(shop.region.name != 'Capacity Upgrade' or world.shuffle_capacity_upgrades[player])
|
(shop.region.name != 'Capacity Upgrade' or world.worlds[player].options.shuffle_capacity_upgrades)
|
||||||
rom.write_byte(0x186560 + shop.sram_offset + slot, 1 if count_shop else 0)
|
rom.write_byte(0x186560 + shop.sram_offset + slot, 1 if count_shop else 0)
|
||||||
if item['item'] == 'Single Arrow' and item['player'] == 0:
|
if item['item'] == 'Single Arrow' and item['player'] == 0:
|
||||||
arrow_mask |= 1 << index
|
arrow_mask |= 1 << index
|
||||||
@@ -1789,7 +1777,7 @@ def write_custom_shops(rom, world, player):
|
|||||||
item_code = get_nonnative_item_sprite(world.worlds[item['player']].item_name_to_id[item['item']])
|
item_code = get_nonnative_item_sprite(world.worlds[item['player']].item_name_to_id[item['item']])
|
||||||
else:
|
else:
|
||||||
item_code = item_table[item["item"]].item_code
|
item_code = item_table[item["item"]].item_code
|
||||||
if item['item'] == 'Single Arrow' and item['player'] == 0 and world.retro_bow[player]:
|
if item['item'] == 'Single Arrow' and item['player'] == 0 and world.worlds[player].options.retro_bow:
|
||||||
rom.write_byte(0x186500 + shop.sram_offset + slot, arrow_mask)
|
rom.write_byte(0x186500 + shop.sram_offset + slot, arrow_mask)
|
||||||
|
|
||||||
item_data = [shop_id, item_code] + price_data + \
|
item_data = [shop_id, item_code] + price_data + \
|
||||||
@@ -1802,7 +1790,7 @@ def write_custom_shops(rom, world, player):
|
|||||||
items_data.extend([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF])
|
items_data.extend([0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF, 0xFF])
|
||||||
rom.write_bytes(0x184900, items_data)
|
rom.write_bytes(0x184900, items_data)
|
||||||
|
|
||||||
if world.retro_bow[player]:
|
if world.worlds[player].options.retro_bow:
|
||||||
retro_shop_slots.append(0xFF)
|
retro_shop_slots.append(0xFF)
|
||||||
rom.write_bytes(0x186540, retro_shop_slots)
|
rom.write_bytes(0x186540, retro_shop_slots)
|
||||||
|
|
||||||
@@ -2207,19 +2195,18 @@ def write_string_to_rom(rom, target, string):
|
|||||||
|
|
||||||
def write_strings(rom, world, player):
|
def write_strings(rom, world, player):
|
||||||
from . import ALTTPWorld
|
from . import ALTTPWorld
|
||||||
|
local_random = world.worlds[player].random
|
||||||
w: ALTTPWorld = world.worlds[player]
|
w: ALTTPWorld = world.worlds[player]
|
||||||
local_random = w.random
|
|
||||||
|
|
||||||
tt = TextTable()
|
tt = TextTable()
|
||||||
tt.removeUnwantedText()
|
tt.removeUnwantedText()
|
||||||
|
|
||||||
# Let's keep this guy's text accurate to the shuffle setting.
|
# Let's keep this guy's text accurate to the shuffle setting.
|
||||||
if world.entrance_shuffle[player] in ['vanilla', 'dungeons_full', 'dungeons_simple', 'dungeons_crossed']:
|
if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_full', 'dungeons_simple', 'dungeons_crossed']:
|
||||||
tt['kakariko_flophouse_man_no_flippers'] = 'I really hate mowing my yard.\n{PAGEBREAK}\nI should move.'
|
tt['kakariko_flophouse_man_no_flippers'] = 'I really hate mowing my yard.\n{PAGEBREAK}\nI should move.'
|
||||||
tt['kakariko_flophouse_man'] = 'I really hate mowing my yard.\n{PAGEBREAK}\nI should move.'
|
tt['kakariko_flophouse_man'] = 'I really hate mowing my yard.\n{PAGEBREAK}\nI should move.'
|
||||||
|
|
||||||
if world.mode[player] == 'inverted':
|
if world.worlds[player].options.mode == 'inverted':
|
||||||
tt['sign_village_of_outcasts'] = 'attention\nferal ducks sighted\nhiding in statues\n\nflute players beware\n'
|
tt['sign_village_of_outcasts'] = 'attention\nferal ducks sighted\nhiding in statues\n\nflute players beware\n'
|
||||||
|
|
||||||
def hint_text(dest, ped_hint=False):
|
def hint_text(dest, ped_hint=False):
|
||||||
@@ -2238,21 +2225,21 @@ def write_strings(rom, world, player):
|
|||||||
hint += f" for {world.player_name[dest.player]}"
|
hint += f" for {world.player_name[dest.player]}"
|
||||||
return hint
|
return hint
|
||||||
|
|
||||||
if world.scams[player].gives_king_zora_hint:
|
if world.worlds[player].options.scams.gives_king_zora_hint:
|
||||||
# Zora hint
|
# Zora hint
|
||||||
zora_location = world.get_location("King Zora", player)
|
zora_location = world.get_location("King Zora", player)
|
||||||
tt['zora_tells_cost'] = f"You got 500 rupees to buy {hint_text(zora_location.item)}" \
|
tt['zora_tells_cost'] = f"You got 500 rupees to buy {hint_text(zora_location.item)}" \
|
||||||
f"\n ≥ Duh\n Oh carp\n{{CHOICE}}"
|
f"\n ≥ Duh\n Oh carp\n{{CHOICE}}"
|
||||||
if world.scams[player].gives_bottle_merchant_hint:
|
if world.worlds[player].options.scams.gives_bottle_merchant_hint:
|
||||||
# Bottle Vendor hint
|
# Bottle Vendor hint
|
||||||
vendor_location = world.get_location("Bottle Merchant", player)
|
vendor_location = world.get_location("Bottle Merchant", player)
|
||||||
tt['bottle_vendor_choice'] = f"I gots {hint_text(vendor_location.item)}\nYous gots 100 rupees?" \
|
tt['bottle_vendor_choice'] = f"I gots {hint_text(vendor_location.item)}\nYous gots 100 rupees?" \
|
||||||
f"\n ≥ I want\n no way!\n{{CHOICE}}"
|
f"\n ≥ I want\n no way!\n{{CHOICE}}"
|
||||||
|
|
||||||
# First we write hints about entrances, some from the inconvenient list others from all reasonable entrances.
|
# First we write hints about entrances, some from the inconvenient list others from all reasonable entrances.
|
||||||
if world.hints[player]:
|
if world.worlds[player].options.hints:
|
||||||
if world.hints[player].value >= 2:
|
if world.worlds[player].options.hints.value >= 2:
|
||||||
if world.hints[player] == "full":
|
if world.worlds[player].options.hints == "full":
|
||||||
tt['sign_north_of_links_house'] = '> Randomizer The telepathic tiles have hints!'
|
tt['sign_north_of_links_house'] = '> Randomizer The telepathic tiles have hints!'
|
||||||
else:
|
else:
|
||||||
tt['sign_north_of_links_house'] = '> Randomizer The telepathic tiles can have hints!'
|
tt['sign_north_of_links_house'] = '> Randomizer The telepathic tiles can have hints!'
|
||||||
@@ -2265,11 +2252,11 @@ def write_strings(rom, world, player):
|
|||||||
entrances_to_hint = {}
|
entrances_to_hint = {}
|
||||||
entrances_to_hint.update(InconvenientDungeonEntrances)
|
entrances_to_hint.update(InconvenientDungeonEntrances)
|
||||||
if world.shuffle_ganon:
|
if world.shuffle_ganon:
|
||||||
if world.mode[player] == 'inverted':
|
if world.worlds[player].options.mode == 'inverted':
|
||||||
entrances_to_hint.update({'Inverted Ganons Tower': 'The sealed castle door'})
|
entrances_to_hint.update({'Inverted Ganons Tower': 'The sealed castle door'})
|
||||||
else:
|
else:
|
||||||
entrances_to_hint.update({'Ganons Tower': 'Ganon\'s Tower'})
|
entrances_to_hint.update({'Ganons Tower': 'Ganon\'s Tower'})
|
||||||
if world.entrance_shuffle[player] in ['simple', 'restricted']:
|
if world.worlds[player].options.entrance_shuffle in ['simple', 'restricted']:
|
||||||
for entrance in all_entrances:
|
for entrance in all_entrances:
|
||||||
if entrance.name in entrances_to_hint:
|
if entrance.name in entrances_to_hint:
|
||||||
this_hint = entrances_to_hint[entrance.name] + ' leads to ' + hint_text(
|
this_hint = entrances_to_hint[entrance.name] + ' leads to ' + hint_text(
|
||||||
@@ -2279,9 +2266,9 @@ def write_strings(rom, world, player):
|
|||||||
break
|
break
|
||||||
# Now we write inconvenient locations for most shuffles and finish taking care of the less chaotic ones.
|
# Now we write inconvenient locations for most shuffles and finish taking care of the less chaotic ones.
|
||||||
entrances_to_hint.update(InconvenientOtherEntrances)
|
entrances_to_hint.update(InconvenientOtherEntrances)
|
||||||
if world.entrance_shuffle[player] in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
|
if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
|
||||||
hint_count = 0
|
hint_count = 0
|
||||||
elif world.entrance_shuffle[player] in ['simple', 'restricted']:
|
elif world.worlds[player].options.entrance_shuffle in ['simple', 'restricted']:
|
||||||
hint_count = 2
|
hint_count = 2
|
||||||
else:
|
else:
|
||||||
hint_count = 4
|
hint_count = 4
|
||||||
@@ -2298,31 +2285,31 @@ def write_strings(rom, world, player):
|
|||||||
|
|
||||||
# Next we handle hints for randomly selected other entrances,
|
# Next we handle hints for randomly selected other entrances,
|
||||||
# curating the selection intelligently based on shuffle.
|
# curating the selection intelligently based on shuffle.
|
||||||
if world.entrance_shuffle[player] not in ['simple', 'restricted']:
|
if world.worlds[player].options.entrance_shuffle not in ['simple', 'restricted']:
|
||||||
entrances_to_hint.update(ConnectorEntrances)
|
entrances_to_hint.update(ConnectorEntrances)
|
||||||
entrances_to_hint.update(DungeonEntrances)
|
entrances_to_hint.update(DungeonEntrances)
|
||||||
if world.mode[player] == 'inverted':
|
if world.worlds[player].options.mode == 'inverted':
|
||||||
entrances_to_hint.update({'Inverted Agahnims Tower': 'The dark mountain tower'})
|
entrances_to_hint.update({'Inverted Agahnims Tower': 'The dark mountain tower'})
|
||||||
else:
|
else:
|
||||||
entrances_to_hint.update({'Agahnims Tower': 'The sealed castle door'})
|
entrances_to_hint.update({'Agahnims Tower': 'The sealed castle door'})
|
||||||
elif world.entrance_shuffle[player] == 'restricted':
|
elif world.worlds[player].options.entrance_shuffle == 'restricted':
|
||||||
entrances_to_hint.update(ConnectorEntrances)
|
entrances_to_hint.update(ConnectorEntrances)
|
||||||
entrances_to_hint.update(OtherEntrances)
|
entrances_to_hint.update(OtherEntrances)
|
||||||
if world.mode[player] == 'inverted':
|
if world.worlds[player].options.mode == 'inverted':
|
||||||
entrances_to_hint.update({'Inverted Dark Sanctuary': 'The dark sanctuary cave'})
|
entrances_to_hint.update({'Inverted Dark Sanctuary': 'The dark sanctuary cave'})
|
||||||
entrances_to_hint.update({'Inverted Big Bomb Shop': 'The old hero\'s dark home'})
|
entrances_to_hint.update({'Inverted Big Bomb Shop': 'The old hero\'s dark home'})
|
||||||
entrances_to_hint.update({'Inverted Links House': 'The old hero\'s light home'})
|
entrances_to_hint.update({'Inverted Links House': 'The old hero\'s light home'})
|
||||||
else:
|
else:
|
||||||
entrances_to_hint.update({'Dark Sanctuary Hint': 'The dark sanctuary cave'})
|
entrances_to_hint.update({'Dark Sanctuary Hint': 'The dark sanctuary cave'})
|
||||||
entrances_to_hint.update({'Big Bomb Shop': 'The old bomb shop'})
|
entrances_to_hint.update({'Big Bomb Shop': 'The old bomb shop'})
|
||||||
if world.entrance_shuffle[player] != 'insanity':
|
if world.worlds[player].options.entrance_shuffle != 'insanity':
|
||||||
entrances_to_hint.update(InsanityEntrances)
|
entrances_to_hint.update(InsanityEntrances)
|
||||||
if world.shuffle_ganon:
|
if world.shuffle_ganon:
|
||||||
if world.mode[player] == 'inverted':
|
if world.worlds[player].options.mode == 'inverted':
|
||||||
entrances_to_hint.update({'Inverted Pyramid Entrance': 'The extra castle passage'})
|
entrances_to_hint.update({'Inverted Pyramid Entrance': 'The extra castle passage'})
|
||||||
else:
|
else:
|
||||||
entrances_to_hint.update({'Pyramid Ledge': 'The pyramid ledge'})
|
entrances_to_hint.update({'Pyramid Ledge': 'The pyramid ledge'})
|
||||||
hint_count = 4 if world.entrance_shuffle[player] not in ['vanilla', 'dungeons_simple', 'dungeons_full',
|
hint_count = 4 if world.worlds[player].options.entrance_shuffle not in ['vanilla', 'dungeons_simple', 'dungeons_full',
|
||||||
'dungeons_crossed'] else 0
|
'dungeons_crossed'] else 0
|
||||||
for entrance in all_entrances:
|
for entrance in all_entrances:
|
||||||
if entrance.name in entrances_to_hint:
|
if entrance.name in entrances_to_hint:
|
||||||
@@ -2337,10 +2324,10 @@ def write_strings(rom, world, player):
|
|||||||
|
|
||||||
# Next we write a few hints for specific inconvenient locations. We don't make many because in entrance this is highly unpredictable.
|
# Next we write a few hints for specific inconvenient locations. We don't make many because in entrance this is highly unpredictable.
|
||||||
locations_to_hint = InconvenientLocations.copy()
|
locations_to_hint = InconvenientLocations.copy()
|
||||||
if world.entrance_shuffle[player] in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
|
if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
|
||||||
locations_to_hint.extend(InconvenientVanillaLocations)
|
locations_to_hint.extend(InconvenientVanillaLocations)
|
||||||
local_random.shuffle(locations_to_hint)
|
local_random.shuffle(locations_to_hint)
|
||||||
hint_count = 3 if world.entrance_shuffle[player] not in ['vanilla', 'dungeons_simple', 'dungeons_full',
|
hint_count = 3 if world.worlds[player].options.entrance_shuffle not in ['vanilla', 'dungeons_simple', 'dungeons_full',
|
||||||
'dungeons_crossed'] else 5
|
'dungeons_crossed'] else 5
|
||||||
for location in locations_to_hint[:hint_count]:
|
for location in locations_to_hint[:hint_count]:
|
||||||
if location == 'Swamp Left':
|
if location == 'Swamp Left':
|
||||||
@@ -2395,15 +2382,15 @@ def write_strings(rom, world, player):
|
|||||||
|
|
||||||
# Lastly we write hints to show where certain interesting items are.
|
# Lastly we write hints to show where certain interesting items are.
|
||||||
items_to_hint = RelevantItems.copy()
|
items_to_hint = RelevantItems.copy()
|
||||||
if world.small_key_shuffle[player].hints_useful:
|
if world.worlds[player].options.small_key_shuffle.hints_useful:
|
||||||
items_to_hint |= item_name_groups["Small Keys"]
|
items_to_hint |= item_name_groups["Small Keys"]
|
||||||
if world.big_key_shuffle[player].hints_useful:
|
if world.worlds[player].options.big_key_shuffle.hints_useful:
|
||||||
items_to_hint |= item_name_groups["Big Keys"]
|
items_to_hint |= item_name_groups["Big Keys"]
|
||||||
|
|
||||||
if world.hints[player] == "full":
|
if world.worlds[player].options.hints == "full":
|
||||||
hint_count = len(hint_locations) # fill all remaining hint locations with Item hints.
|
hint_count = len(hint_locations) # fill all remaining hint locations with Item hints.
|
||||||
else:
|
else:
|
||||||
hint_count = 5 if world.entrance_shuffle[player] not in ['vanilla', 'dungeons_simple', 'dungeons_full',
|
hint_count = 5 if world.worlds[player].options.entrance_shuffle not in ['vanilla', 'dungeons_simple', 'dungeons_full',
|
||||||
'dungeons_crossed'] else 8
|
'dungeons_crossed'] else 8
|
||||||
hint_count = min(hint_count, len(items_to_hint), len(hint_locations))
|
hint_count = min(hint_count, len(items_to_hint), len(hint_locations))
|
||||||
if hint_count:
|
if hint_count:
|
||||||
@@ -2434,7 +2421,7 @@ def write_strings(rom, world, player):
|
|||||||
tt['ganon_phase_3_no_silvers'] = 'Did you find the silver arrows%s' % silverarrow_hint
|
tt['ganon_phase_3_no_silvers'] = 'Did you find the silver arrows%s' % silverarrow_hint
|
||||||
tt['ganon_phase_3_no_silvers_alt'] = 'Did you find the silver arrows%s' % silverarrow_hint
|
tt['ganon_phase_3_no_silvers_alt'] = 'Did you find the silver arrows%s' % silverarrow_hint
|
||||||
if world.worlds[player].has_progressive_bows and (w.difficulty_requirements.progressive_bow_limit >= 2 or (
|
if world.worlds[player].has_progressive_bows and (w.difficulty_requirements.progressive_bow_limit >= 2 or (
|
||||||
world.swordless[player] or world.glitches_required[player] == 'no_glitches')):
|
world.worlds[player].options.swordless or world.worlds[player].options.glitches_required == 'no_glitches')):
|
||||||
prog_bow_locs = world.find_item_locations('Progressive Bow', player, True)
|
prog_bow_locs = world.find_item_locations('Progressive Bow', player, True)
|
||||||
local_random.shuffle(prog_bow_locs)
|
local_random.shuffle(prog_bow_locs)
|
||||||
found_bow = False
|
found_bow = False
|
||||||
@@ -2458,26 +2445,26 @@ def write_strings(rom, world, player):
|
|||||||
greenpendant = world.find_item('Green Pendant', player)
|
greenpendant = world.find_item('Green Pendant', player)
|
||||||
tt['sahasrahla_bring_courage'] = 'I lost my family heirloom in %s' % greenpendant.hint_text
|
tt['sahasrahla_bring_courage'] = 'I lost my family heirloom in %s' % greenpendant.hint_text
|
||||||
|
|
||||||
if world.crystals_needed_for_gt[player] == 1:
|
if world.worlds[player].options.crystals_needed_for_gt == 1:
|
||||||
tt['sign_ganons_tower'] = 'You need a crystal to enter.'
|
tt['sign_ganons_tower'] = 'You need a crystal to enter.'
|
||||||
else:
|
else:
|
||||||
tt['sign_ganons_tower'] = f'You need {world.crystals_needed_for_gt[player]} crystals to enter.'
|
tt['sign_ganons_tower'] = f'You need {world.worlds[player].options.crystals_needed_for_gt} crystals to enter.'
|
||||||
|
|
||||||
if world.goal[player] == 'bosses':
|
if world.worlds[player].options.goal == 'bosses':
|
||||||
tt['sign_ganon'] = 'You need to kill all bosses, Ganon last.'
|
tt['sign_ganon'] = 'You need to kill all bosses, Ganon last.'
|
||||||
elif world.goal[player] == 'ganon_pedestal':
|
elif world.worlds[player].options.goal == 'ganon_pedestal':
|
||||||
tt['sign_ganon'] = 'You need to pull the pedestal to defeat Ganon.'
|
tt['sign_ganon'] = 'You need to pull the pedestal to defeat Ganon.'
|
||||||
elif world.goal[player] == "ganon":
|
elif world.worlds[player].options.goal == "ganon":
|
||||||
if world.crystals_needed_for_ganon[player] == 1:
|
if world.worlds[player].options.crystals_needed_for_ganon == 1:
|
||||||
tt['sign_ganon'] = 'You need a crystal to beat Ganon and have beaten Agahnim atop Ganons Tower.'
|
tt['sign_ganon'] = 'You need a crystal to beat Ganon and have beaten Agahnim atop Ganons Tower.'
|
||||||
else:
|
else:
|
||||||
tt['sign_ganon'] = f'You need {world.crystals_needed_for_ganon[player]} crystals to beat Ganon and ' \
|
tt['sign_ganon'] = f'You need {world.worlds[player].options.crystals_needed_for_ganon} crystals to beat Ganon and ' \
|
||||||
f'have beaten Agahnim atop Ganons Tower'
|
f'have beaten Agahnim atop Ganons Tower'
|
||||||
else:
|
else:
|
||||||
if world.crystals_needed_for_ganon[player] == 1:
|
if world.worlds[player].options.crystals_needed_for_ganon == 1:
|
||||||
tt['sign_ganon'] = 'You need a crystal to beat Ganon.'
|
tt['sign_ganon'] = 'You need a crystal to beat Ganon.'
|
||||||
else:
|
else:
|
||||||
tt['sign_ganon'] = f'You need {world.crystals_needed_for_ganon[player]} crystals to beat Ganon.'
|
tt['sign_ganon'] = f'You need {world.worlds[player].options.crystals_needed_for_ganon} crystals to beat Ganon.'
|
||||||
|
|
||||||
tt['uncle_leaving_text'] = Uncle_texts[local_random.randint(0, len(Uncle_texts) - 1)]
|
tt['uncle_leaving_text'] = Uncle_texts[local_random.randint(0, len(Uncle_texts) - 1)]
|
||||||
tt['end_triforce'] = "{NOBORDER}\n" + Triforce_texts[local_random.randint(0, len(Triforce_texts) - 1)]
|
tt['end_triforce'] = "{NOBORDER}\n" + Triforce_texts[local_random.randint(0, len(Triforce_texts) - 1)]
|
||||||
@@ -2490,10 +2477,10 @@ def write_strings(rom, world, player):
|
|||||||
triforce_pieces_required = max(0, w.treasure_hunt_required -
|
triforce_pieces_required = max(0, w.treasure_hunt_required -
|
||||||
sum(1 for item in world.precollected_items[player] if item.name == "Triforce Piece"))
|
sum(1 for item in world.precollected_items[player] if item.name == "Triforce Piece"))
|
||||||
|
|
||||||
if world.goal[player] in ['triforce_hunt', 'local_triforce_hunt']:
|
if world.worlds[player].options.goal in ['triforce_hunt', 'local_triforce_hunt']:
|
||||||
tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Get the Triforce Pieces.'
|
tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Get the Triforce Pieces.'
|
||||||
tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.'
|
tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.'
|
||||||
if world.goal[player] == 'triforce_hunt' and world.players > 1:
|
if world.worlds[player].options.goal == 'triforce_hunt' and world.players > 1:
|
||||||
tt['sign_ganon'] = 'Go find the Triforce pieces with your friends... Ganon is invincible!'
|
tt['sign_ganon'] = 'Go find the Triforce pieces with your friends... Ganon is invincible!'
|
||||||
else:
|
else:
|
||||||
tt['sign_ganon'] = 'Go find the Triforce pieces... Ganon is invincible!'
|
tt['sign_ganon'] = 'Go find the Triforce pieces... Ganon is invincible!'
|
||||||
@@ -2507,7 +2494,7 @@ def write_strings(rom, world, player):
|
|||||||
"invisibility.\n\n\n\n… … …\n\nWait! you can see me? I knew I should have\n" \
|
"invisibility.\n\n\n\n… … …\n\nWait! you can see me? I knew I should have\n" \
|
||||||
"hidden in a hollow tree. If you bring\n%d Triforce piece out of %d, I can reassemble it." % \
|
"hidden in a hollow tree. If you bring\n%d Triforce piece out of %d, I can reassemble it." % \
|
||||||
(triforce_pieces_required, w.treasure_hunt_total)
|
(triforce_pieces_required, w.treasure_hunt_total)
|
||||||
elif world.goal[player] in ['pedestal']:
|
elif world.worlds[player].options.goal in ['pedestal']:
|
||||||
tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Your goal is at the pedestal.'
|
tt['ganon_fall_in_alt'] = 'Why are you even here?\n You can\'t even hurt me! Your goal is at the pedestal.'
|
||||||
tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.'
|
tt['ganon_phase_3_alt'] = 'Seriously? Go Away, I will not Die.'
|
||||||
tt['sign_ganon'] = 'You need to get to the pedestal... Ganon is invincible!'
|
tt['sign_ganon'] = 'You need to get to the pedestal... Ganon is invincible!'
|
||||||
@@ -2516,17 +2503,17 @@ def write_strings(rom, world, player):
|
|||||||
tt['ganon_fall_in_alt'] = 'You cannot defeat me until you finish your goal!'
|
tt['ganon_fall_in_alt'] = 'You cannot defeat me until you finish your goal!'
|
||||||
tt['ganon_phase_3_alt'] = 'Got wax in\nyour ears?\nI can not die!'
|
tt['ganon_phase_3_alt'] = 'Got wax in\nyour ears?\nI can not die!'
|
||||||
if triforce_pieces_required > 1:
|
if triforce_pieces_required > 1:
|
||||||
if world.goal[player] == 'ganon_triforce_hunt' and world.players > 1:
|
if world.worlds[player].options.goal == 'ganon_triforce_hunt' and world.players > 1:
|
||||||
tt['sign_ganon'] = 'You need to find %d Triforce pieces out of %d with your friends to defeat Ganon.' % \
|
tt['sign_ganon'] = 'You need to find %d Triforce pieces out of %d with your friends to defeat Ganon.' % \
|
||||||
(triforce_pieces_required, w.treasure_hunt_total)
|
(triforce_pieces_required, w.treasure_hunt_total)
|
||||||
elif world.goal[player] in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']:
|
elif world.worlds[player].options.goal in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']:
|
||||||
tt['sign_ganon'] = 'You need to find %d Triforce pieces out of %d to defeat Ganon.' % \
|
tt['sign_ganon'] = 'You need to find %d Triforce pieces out of %d to defeat Ganon.' % \
|
||||||
(triforce_pieces_required, w.treasure_hunt_total)
|
(triforce_pieces_required, w.treasure_hunt_total)
|
||||||
else:
|
else:
|
||||||
if world.goal[player] == 'ganon_triforce_hunt' and world.players > 1:
|
if world.worlds[player].options.goal == 'ganon_triforce_hunt' and world.players > 1:
|
||||||
tt['sign_ganon'] = 'You need to find %d Triforce piece out of %d with your friends to defeat Ganon.' % \
|
tt['sign_ganon'] = 'You need to find %d Triforce piece out of %d with your friends to defeat Ganon.' % \
|
||||||
(triforce_pieces_required, w.treasure_hunt_total)
|
(triforce_pieces_required, w.treasure_hunt_total)
|
||||||
elif world.goal[player] in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']:
|
elif world.worlds[player].options.goal in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']:
|
||||||
tt['sign_ganon'] = 'You need to find %d Triforce piece out of %d to defeat Ganon.' % \
|
tt['sign_ganon'] = 'You need to find %d Triforce piece out of %d to defeat Ganon.' % \
|
||||||
(triforce_pieces_required, w.treasure_hunt_total)
|
(triforce_pieces_required, w.treasure_hunt_total)
|
||||||
|
|
||||||
@@ -2549,11 +2536,11 @@ def write_strings(rom, world, player):
|
|||||||
tt['tablet_bombos_book'] = bombos_text
|
tt['tablet_bombos_book'] = bombos_text
|
||||||
|
|
||||||
# inverted spawn menu changes
|
# inverted spawn menu changes
|
||||||
if world.mode[player] == 'inverted':
|
if world.worlds[player].options.mode == 'inverted':
|
||||||
tt['menu_start_2'] = "{MENU}\n{SPEED0}\n≥@'s house\n Dark Chapel\n{CHOICE3}"
|
tt['menu_start_2'] = "{MENU}\n{SPEED0}\n≥@'s house\n Dark Chapel\n{CHOICE3}"
|
||||||
tt['menu_start_3'] = "{MENU}\n{SPEED0}\n≥@'s house\n Dark Chapel\n Mountain Cave\n{CHOICE2}"
|
tt['menu_start_3'] = "{MENU}\n{SPEED0}\n≥@'s house\n Dark Chapel\n Mountain Cave\n{CHOICE2}"
|
||||||
|
|
||||||
for at, text, _ in world.plando_texts[player]:
|
for at, text, _ in world.worlds[player].options.plando_texts:
|
||||||
|
|
||||||
if at not in tt:
|
if at not in tt:
|
||||||
raise Exception(f"No text target \"{at}\" found.")
|
raise Exception(f"No text target \"{at}\" found.")
|
||||||
@@ -2626,12 +2613,12 @@ def set_inverted_mode(world, player, rom):
|
|||||||
rom.write_byte(snes_to_pc(0x08D40C), 0xD0) # morph proof
|
rom.write_byte(snes_to_pc(0x08D40C), 0xD0) # morph proof
|
||||||
# the following bytes should only be written in vanilla
|
# the following bytes should only be written in vanilla
|
||||||
# or they'll overwrite the randomizer's shuffles
|
# or they'll overwrite the randomizer's shuffles
|
||||||
if world.entrance_shuffle[player] == 'vanilla':
|
if world.worlds[player].options.entrance_shuffle == 'vanilla':
|
||||||
rom.write_byte(0xDBB73 + 0x23, 0x37) # switch AT and GT
|
rom.write_byte(0xDBB73 + 0x23, 0x37) # switch AT and GT
|
||||||
rom.write_byte(0xDBB73 + 0x36, 0x24)
|
rom.write_byte(0xDBB73 + 0x36, 0x24)
|
||||||
rom.write_int16(0x15AEE + 2 * 0x38, 0x00E0)
|
rom.write_int16(0x15AEE + 2 * 0x38, 0x00E0)
|
||||||
rom.write_int16(0x15AEE + 2 * 0x25, 0x000C)
|
rom.write_int16(0x15AEE + 2 * 0x25, 0x000C)
|
||||||
if world.entrance_shuffle[player] in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
|
if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
|
||||||
rom.write_byte(0x15B8C, 0x6C)
|
rom.write_byte(0x15B8C, 0x6C)
|
||||||
rom.write_byte(0xDBB73 + 0x00, 0x53) # switch bomb shop and links house
|
rom.write_byte(0xDBB73 + 0x00, 0x53) # switch bomb shop and links house
|
||||||
rom.write_byte(0xDBB73 + 0x52, 0x01)
|
rom.write_byte(0xDBB73 + 0x52, 0x01)
|
||||||
@@ -2689,7 +2676,7 @@ def set_inverted_mode(world, player, rom):
|
|||||||
rom.write_int16(snes_to_pc(0x02D9A6), 0x005A)
|
rom.write_int16(snes_to_pc(0x02D9A6), 0x005A)
|
||||||
rom.write_byte(snes_to_pc(0x02D9B3), 0x12)
|
rom.write_byte(snes_to_pc(0x02D9B3), 0x12)
|
||||||
# keep the old man spawn point at old man house unless shuffle is vanilla
|
# keep the old man spawn point at old man house unless shuffle is vanilla
|
||||||
if world.entrance_shuffle[player] in ['vanilla', 'dungeons_full', 'dungeons_simple', 'dungeons_crossed']:
|
if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_full', 'dungeons_simple', 'dungeons_crossed']:
|
||||||
rom.write_bytes(snes_to_pc(0x308350), [0x00, 0x00, 0x01])
|
rom.write_bytes(snes_to_pc(0x308350), [0x00, 0x00, 0x01])
|
||||||
rom.write_int16(snes_to_pc(0x02D8DE), 0x00F1)
|
rom.write_int16(snes_to_pc(0x02D8DE), 0x00F1)
|
||||||
rom.write_bytes(snes_to_pc(0x02D910), [0x1F, 0x1E, 0x1F, 0x1F, 0x03, 0x02, 0x03, 0x03])
|
rom.write_bytes(snes_to_pc(0x02D910), [0x1F, 0x1E, 0x1F, 0x1F, 0x03, 0x02, 0x03, 0x03])
|
||||||
@@ -2752,7 +2739,7 @@ def set_inverted_mode(world, player, rom):
|
|||||||
rom.write_int16s(snes_to_pc(0x1bb836), [0x001B, 0x001B, 0x001B])
|
rom.write_int16s(snes_to_pc(0x1bb836), [0x001B, 0x001B, 0x001B])
|
||||||
rom.write_int16(snes_to_pc(0x308300), 0x0140) # new pyramid hole entrance
|
rom.write_int16(snes_to_pc(0x308300), 0x0140) # new pyramid hole entrance
|
||||||
rom.write_int16(snes_to_pc(0x308320), 0x001B)
|
rom.write_int16(snes_to_pc(0x308320), 0x001B)
|
||||||
if world.entrance_shuffle[player] in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
|
if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
|
||||||
rom.write_byte(snes_to_pc(0x308340), 0x7B)
|
rom.write_byte(snes_to_pc(0x308340), 0x7B)
|
||||||
rom.write_int16(snes_to_pc(0x1af504), 0x148B)
|
rom.write_int16(snes_to_pc(0x1af504), 0x148B)
|
||||||
rom.write_int16(snes_to_pc(0x1af50c), 0x149B)
|
rom.write_int16(snes_to_pc(0x1af50c), 0x149B)
|
||||||
@@ -2789,10 +2776,10 @@ def set_inverted_mode(world, player, rom):
|
|||||||
rom.write_bytes(snes_to_pc(0x1BC85A), [0x50, 0x0F, 0x82])
|
rom.write_bytes(snes_to_pc(0x1BC85A), [0x50, 0x0F, 0x82])
|
||||||
rom.write_int16(0xDB96F + 2 * 0x35, 0x001B) # move pyramid exit door
|
rom.write_int16(0xDB96F + 2 * 0x35, 0x001B) # move pyramid exit door
|
||||||
rom.write_int16(0xDBA71 + 2 * 0x35, 0x06A4)
|
rom.write_int16(0xDBA71 + 2 * 0x35, 0x06A4)
|
||||||
if world.entrance_shuffle[player] in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
|
if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
|
||||||
rom.write_byte(0xDBB73 + 0x35, 0x36)
|
rom.write_byte(0xDBB73 + 0x35, 0x36)
|
||||||
rom.write_byte(snes_to_pc(0x09D436), 0xF3) # remove castle gate warp
|
rom.write_byte(snes_to_pc(0x09D436), 0xF3) # remove castle gate warp
|
||||||
if world.entrance_shuffle[player] in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
|
if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
|
||||||
rom.write_int16(0x15AEE + 2 * 0x37, 0x0010) # pyramid exit to new hc area
|
rom.write_int16(0x15AEE + 2 * 0x37, 0x0010) # pyramid exit to new hc area
|
||||||
rom.write_byte(0x15B8C + 0x37, 0x1B)
|
rom.write_byte(0x15B8C + 0x37, 0x1B)
|
||||||
rom.write_int16(0x15BDB + 2 * 0x37, 0x0418)
|
rom.write_int16(0x15BDB + 2 * 0x37, 0x0418)
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ import logging
|
|||||||
from typing import Iterator, Set
|
from typing import Iterator, Set
|
||||||
|
|
||||||
from Options import ItemsAccessibility
|
from Options import ItemsAccessibility
|
||||||
from BaseClasses import Entrance, MultiWorld
|
from BaseClasses import MultiWorld
|
||||||
from worlds.generic.Rules import (add_item_rule, add_rule, forbid_item,
|
from worlds.generic.Rules import (add_item_rule, add_rule, forbid_item,
|
||||||
item_name_in_location_names, location_item_name, set_rule, allow_self_locking_items)
|
item_name_in_location_names, location_item_name, set_rule, allow_self_locking_items)
|
||||||
|
|
||||||
@@ -27,9 +27,9 @@ from .UnderworldGlitchRules import underworld_glitches_rules
|
|||||||
def set_rules(world):
|
def set_rules(world):
|
||||||
player = world.player
|
player = world.player
|
||||||
world = world.multiworld
|
world = world.multiworld
|
||||||
if world.glitches_required[player] == 'no_logic':
|
if world.worlds[player].options.glitches_required == 'no_logic':
|
||||||
if player == next(player_id for player_id in world.get_game_players("A Link to the Past")
|
if player == next(player_id for player_id in world.get_game_players("A Link to the Past")
|
||||||
if world.glitches_required[player_id] == 'no_logic'): # only warn one time
|
if world.worlds[player_id].options.glitches_required == 'no_logic'): # only warn one time
|
||||||
logging.info(
|
logging.info(
|
||||||
'WARNING! Seeds generated under this logic often require major glitches and may be impossible!')
|
'WARNING! Seeds generated under this logic often require major glitches and may be impossible!')
|
||||||
|
|
||||||
@@ -40,8 +40,8 @@ def set_rules(world):
|
|||||||
else:
|
else:
|
||||||
# Set access rules according to max glitches for multiworld progression.
|
# Set access rules according to max glitches for multiworld progression.
|
||||||
# Set accessibility to none, and shuffle assuming the no logic players can always win
|
# Set accessibility to none, and shuffle assuming the no logic players can always win
|
||||||
world.accessibility[player].value = ItemsAccessibility.option_minimal
|
world.worlds[player].options.accessibility.value = ItemsAccessibility.option_minimal
|
||||||
world.progression_balancing[player].value = 0
|
world.worlds[player].options.progression_balancing.value = 0
|
||||||
|
|
||||||
else:
|
else:
|
||||||
world.completion_condition[player] = lambda state: state.has('Triforce', player)
|
world.completion_condition[player] = lambda state: state.has('Triforce', player)
|
||||||
@@ -49,52 +49,52 @@ def set_rules(world):
|
|||||||
dungeon_boss_rules(world, player)
|
dungeon_boss_rules(world, player)
|
||||||
global_rules(world, player)
|
global_rules(world, player)
|
||||||
|
|
||||||
if world.mode[player] != 'inverted':
|
if world.worlds[player].options.mode != 'inverted':
|
||||||
default_rules(world, player)
|
default_rules(world, player)
|
||||||
|
|
||||||
if world.mode[player] == 'open':
|
if world.worlds[player].options.mode == 'open':
|
||||||
open_rules(world, player)
|
open_rules(world, player)
|
||||||
elif world.mode[player] == 'standard':
|
elif world.worlds[player].options.mode == 'standard':
|
||||||
standard_rules(world, player)
|
standard_rules(world, player)
|
||||||
elif world.mode[player] == 'inverted':
|
elif world.worlds[player].options.mode == 'inverted':
|
||||||
open_rules(world, player)
|
open_rules(world, player)
|
||||||
inverted_rules(world, player)
|
inverted_rules(world, player)
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError(f'World state {world.mode[player]} is not implemented yet')
|
raise NotImplementedError(f'World state {world.worlds[player].options.mode} is not implemented yet')
|
||||||
|
|
||||||
if world.glitches_required[player] == 'no_glitches':
|
if world.worlds[player].options.glitches_required == 'no_glitches':
|
||||||
no_glitches_rules(world, player)
|
no_glitches_rules(world, player)
|
||||||
forbid_bomb_jump_requirements(world, player)
|
forbid_bomb_jump_requirements(world, player)
|
||||||
elif world.glitches_required[player] == 'overworld_glitches':
|
elif world.worlds[player].options.glitches_required == 'overworld_glitches':
|
||||||
# Initially setting no_glitches_rules to set the baseline rules for some
|
# Initially setting no_glitches_rules to set the baseline rules for some
|
||||||
# entrances. The overworld_glitches_rules set is primarily additive.
|
# entrances. The overworld_glitches_rules set is primarily additive.
|
||||||
no_glitches_rules(world, player)
|
no_glitches_rules(world, player)
|
||||||
fake_flipper_rules(world, player)
|
fake_flipper_rules(world, player)
|
||||||
overworld_glitches_rules(world, player)
|
overworld_glitches_rules(world, player)
|
||||||
forbid_bomb_jump_requirements(world, player)
|
forbid_bomb_jump_requirements(world, player)
|
||||||
elif world.glitches_required[player] in ['hybrid_major_glitches', 'no_logic']:
|
elif world.worlds[player].options.glitches_required in ['hybrid_major_glitches', 'no_logic']:
|
||||||
no_glitches_rules(world, player)
|
no_glitches_rules(world, player)
|
||||||
fake_flipper_rules(world, player)
|
fake_flipper_rules(world, player)
|
||||||
overworld_glitches_rules(world, player)
|
overworld_glitches_rules(world, player)
|
||||||
underworld_glitches_rules(world, player)
|
underworld_glitches_rules(world, player)
|
||||||
bomb_jump_requirements(world, player)
|
bomb_jump_requirements(world, player)
|
||||||
elif world.glitches_required[player] == 'minor_glitches':
|
elif world.worlds[player].options.glitches_required == 'minor_glitches':
|
||||||
no_glitches_rules(world, player)
|
no_glitches_rules(world, player)
|
||||||
fake_flipper_rules(world, player)
|
fake_flipper_rules(world, player)
|
||||||
forbid_bomb_jump_requirements(world, player)
|
forbid_bomb_jump_requirements(world, player)
|
||||||
else:
|
else:
|
||||||
raise NotImplementedError(f'Not implemented yet: Logic - {world.glitches_required[player]}')
|
raise NotImplementedError(f'Not implemented yet: Logic - {world.worlds[player].options.glitches_required}')
|
||||||
|
|
||||||
if world.goal[player] == 'bosses':
|
if world.worlds[player].options.goal == 'bosses':
|
||||||
# require all bosses to beat ganon
|
# require all bosses to beat ganon
|
||||||
add_rule(world.get_location('Ganon', player), lambda state: state.can_reach('Master Sword Pedestal', 'Location', player) and state.has('Beat Agahnim 1', player) and state.has('Beat Agahnim 2', player) and has_crystals(state, 7, player))
|
add_rule(world.get_location('Ganon', player), lambda state: state.can_reach('Master Sword Pedestal', 'Location', player) and state.has('Beat Agahnim 1', player) and state.has('Beat Agahnim 2', player) and has_crystals(state, 7, player))
|
||||||
elif world.goal[player] == 'ganon':
|
elif world.worlds[player].options.goal == 'ganon':
|
||||||
# require aga2 to beat ganon
|
# require aga2 to beat ganon
|
||||||
add_rule(world.get_location('Ganon', player), lambda state: state.has('Beat Agahnim 2', player))
|
add_rule(world.get_location('Ganon', player), lambda state: state.has('Beat Agahnim 2', player))
|
||||||
|
|
||||||
if world.mode[player] != 'inverted':
|
if world.worlds[player].options.mode != 'inverted':
|
||||||
set_big_bomb_rules(world, player)
|
set_big_bomb_rules(world, player)
|
||||||
if world.glitches_required[player].current_key in {'overworld_glitches', 'hybrid_major_glitches', 'no_logic'} and world.entrance_shuffle[player].current_key not in {'insanity', 'insanity_legacy', 'madness'}:
|
if world.worlds[player].options.glitches_required.current_key in {'overworld_glitches', 'hybrid_major_glitches', 'no_logic'} and world.worlds[player].options.entrance_shuffle.current_key not in {'insanity', 'insanity_legacy', 'madness'}:
|
||||||
path_to_courtyard = mirrorless_path_to_castle_courtyard(world, player)
|
path_to_courtyard = mirrorless_path_to_castle_courtyard(world, player)
|
||||||
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.multiworld.get_entrance('Dark Death Mountain Offset Mirror', player).can_reach(state) and all(rule(state) for rule in path_to_courtyard), 'or')
|
add_rule(world.get_entrance('Pyramid Fairy', player), lambda state: state.multiworld.get_entrance('Dark Death Mountain Offset Mirror', player).can_reach(state) and all(rule(state) for rule in path_to_courtyard), 'or')
|
||||||
else:
|
else:
|
||||||
@@ -102,21 +102,24 @@ def set_rules(world):
|
|||||||
|
|
||||||
# if swamp and dam have not been moved we require mirror for swamp palace
|
# if swamp and dam have not been moved we require mirror for swamp palace
|
||||||
# however there is mirrorless swamp in hybrid MG, so we don't necessarily want this. HMG handles this requirement itself.
|
# however there is mirrorless swamp in hybrid MG, so we don't necessarily want this. HMG handles this requirement itself.
|
||||||
if not world.worlds[player].swamp_patch_required and world.glitches_required[player] not in ['hybrid_major_glitches', 'no_logic']:
|
if not world.worlds[player].swamp_patch_required and world.worlds[player].options.glitches_required not in ['hybrid_major_glitches', 'no_logic']:
|
||||||
add_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player))
|
add_rule(world.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Magic Mirror', player))
|
||||||
|
|
||||||
# GT Entrance may be required for Turtle Rock for OWG and < 7 required
|
# GT Entrance may be required for Turtle Rock for OWG and < 7 required
|
||||||
ganons_tower = world.get_entrance('Inverted Ganons Tower' if world.mode[player] == 'inverted' else 'Ganons Tower', player)
|
ganons_tower = world.get_entrance('Inverted Ganons Tower' if world.worlds[player].options.mode == 'inverted' else 'Ganons Tower', player)
|
||||||
if world.crystals_needed_for_gt[player] == 7 and not (world.glitches_required[player] in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic'] and world.mode[player] != 'inverted'):
|
if (world.worlds[player].options.crystals_needed_for_gt == 7
|
||||||
|
and not (world.worlds[player].options.glitches_required
|
||||||
|
in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']
|
||||||
|
and world.worlds[player].options.mode != 'inverted')):
|
||||||
set_rule(ganons_tower, lambda state: False)
|
set_rule(ganons_tower, lambda state: False)
|
||||||
|
|
||||||
set_trock_key_rules(world, player)
|
set_trock_key_rules(world, player)
|
||||||
|
|
||||||
set_rule(ganons_tower, lambda state: has_crystals(state, state.multiworld.crystals_needed_for_gt[player], player))
|
set_rule(ganons_tower, lambda state: has_crystals(state, state.multiworld.worlds[player].options.crystals_needed_for_gt, player))
|
||||||
if world.mode[player] != 'inverted' and world.glitches_required[player] in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']:
|
if world.worlds[player].options.mode != 'inverted' and world.worlds[player].options.glitches_required in ['overworld_glitches', 'hybrid_major_glitches', 'no_logic']:
|
||||||
add_rule(world.get_entrance('Ganons Tower', player), lambda state: state.multiworld.get_entrance('Ganons Tower Ascent', player).can_reach(state), 'or')
|
add_rule(world.get_entrance('Ganons Tower', player), lambda state: state.multiworld.get_entrance('Ganons Tower Ascent', player).can_reach(state), 'or')
|
||||||
|
|
||||||
set_bunny_rules(world, player, world.mode[player] == 'inverted')
|
set_bunny_rules(world, player, world.worlds[player].options.mode == 'inverted')
|
||||||
|
|
||||||
|
|
||||||
def mirrorless_path_to_castle_courtyard(world, player):
|
def mirrorless_path_to_castle_courtyard(world, player):
|
||||||
@@ -150,17 +153,17 @@ def set_always_allow(spot, rule):
|
|||||||
|
|
||||||
|
|
||||||
def add_lamp_requirement(world: MultiWorld, spot, player: int, has_accessible_torch: bool = False):
|
def add_lamp_requirement(world: MultiWorld, spot, player: int, has_accessible_torch: bool = False):
|
||||||
if world.dark_room_logic[player] == "lamp":
|
if world.worlds[player].options.dark_room_logic == "lamp":
|
||||||
add_rule(spot, lambda state: state.has('Lamp', player))
|
add_rule(spot, lambda state: state.has('Lamp', player))
|
||||||
elif world.dark_room_logic[player] == "torches": # implicitly lamp as well
|
elif world.worlds[player].options.dark_room_logic == "torches": # implicitly lamp as well
|
||||||
if has_accessible_torch:
|
if has_accessible_torch:
|
||||||
add_rule(spot, lambda state: state.has('Lamp', player) or state.has('Fire Rod', player))
|
add_rule(spot, lambda state: state.has('Lamp', player) or state.has('Fire Rod', player))
|
||||||
else:
|
else:
|
||||||
add_rule(spot, lambda state: state.has('Lamp', player))
|
add_rule(spot, lambda state: state.has('Lamp', player))
|
||||||
elif world.dark_room_logic[player] == "none":
|
elif world.worlds[player].options.dark_room_logic == "none":
|
||||||
pass
|
pass
|
||||||
else:
|
else:
|
||||||
raise ValueError(f"Unknown Dark Room Logic: {world.dark_room_logic[player]}")
|
raise ValueError(f"Unknown Dark Room Logic: {world.worlds[player].options.dark_room_logic}")
|
||||||
|
|
||||||
|
|
||||||
non_crossover_items = (item_name_groups["Small Keys"] | item_name_groups["Big Keys"] | progression_items) - {
|
non_crossover_items = (item_name_groups["Small Keys"] | item_name_groups["Big Keys"] | progression_items) - {
|
||||||
@@ -227,12 +230,13 @@ def global_rules(multiworld: MultiWorld, player: int):
|
|||||||
set_rule(multiworld.get_location('Sick Kid', player), lambda state: state.has_group("Bottles", player))
|
set_rule(multiworld.get_location('Sick Kid', player), lambda state: state.has_group("Bottles", player))
|
||||||
set_rule(multiworld.get_location('Library', player), lambda state: state.has('Pegasus Boots', player))
|
set_rule(multiworld.get_location('Library', player), lambda state: state.has('Pegasus Boots', player))
|
||||||
|
|
||||||
if multiworld.enemy_shuffle[player]:
|
if world.options.enemy_shuffle:
|
||||||
set_rule(multiworld.get_location('Mimic Cave', player), lambda state: state.has('Hammer', player) and
|
set_rule(multiworld.get_location('Mimic Cave', player), lambda state: state.has('Hammer', player) and
|
||||||
can_kill_most_things(state, player, 4))
|
can_kill_most_things(state, player, 4))
|
||||||
else:
|
else:
|
||||||
set_rule(multiworld.get_location('Mimic Cave', player), lambda state: state.has('Hammer', player)
|
set_rule(multiworld.get_location('Mimic Cave', player), lambda state: state.has('Hammer', player)
|
||||||
and ((state.multiworld.enemy_health[player] in ("easy", "default") and can_use_bombs(state, player, 4))
|
and ((state.multiworld.worlds[player].options.enemy_health in ("easy", "default")
|
||||||
|
and can_use_bombs(state, player, 4))
|
||||||
or can_shoot_arrows(state, player) or state.has("Cane of Somaria", player)
|
or can_shoot_arrows(state, player) or state.has("Cane of Somaria", player)
|
||||||
or has_beam_sword(state, player)))
|
or has_beam_sword(state, player)))
|
||||||
|
|
||||||
@@ -299,8 +303,7 @@ def global_rules(multiworld: MultiWorld, player: int):
|
|||||||
|
|
||||||
set_rule(multiworld.get_entrance('Sewers Door', player),
|
set_rule(multiworld.get_entrance('Sewers Door', player),
|
||||||
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 4) or (
|
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 4) or (
|
||||||
multiworld.small_key_shuffle[player] == small_key_shuffle.option_universal and multiworld.mode[
|
world.options.small_key_shuffle == small_key_shuffle.option_universal and world.options.mode == 'standard')) # standard universal small keys cannot access the shop
|
||||||
player] == 'standard')) # standard universal small keys cannot access the shop
|
|
||||||
set_rule(multiworld.get_entrance('Sewers Back Door', player),
|
set_rule(multiworld.get_entrance('Sewers Back Door', player),
|
||||||
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 4))
|
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 4))
|
||||||
set_rule(multiworld.get_entrance('Sewers Secret Room', player), lambda state: can_bomb_or_bonk(state, player))
|
set_rule(multiworld.get_entrance('Sewers Secret Room', player), lambda state: can_bomb_or_bonk(state, player))
|
||||||
@@ -339,12 +342,12 @@ def global_rules(multiworld: MultiWorld, player: int):
|
|||||||
add_rule(ep_prize, lambda state: state.has('Big Key (Eastern Palace)', player) and
|
add_rule(ep_prize, lambda state: state.has('Big Key (Eastern Palace)', player) and
|
||||||
state._lttp_has_key('Small Key (Eastern Palace)', player, 2) and
|
state._lttp_has_key('Small Key (Eastern Palace)', player, 2) and
|
||||||
ep_prize.parent_region.dungeon.boss.can_defeat(state))
|
ep_prize.parent_region.dungeon.boss.can_defeat(state))
|
||||||
if not multiworld.enemy_shuffle[player]:
|
if not world.options.enemy_shuffle:
|
||||||
add_rule(ep_boss, lambda state: can_shoot_arrows(state, player))
|
add_rule(ep_boss, lambda state: can_shoot_arrows(state, player))
|
||||||
add_rule(ep_prize, lambda state: can_shoot_arrows(state, player))
|
add_rule(ep_prize, lambda state: can_shoot_arrows(state, player))
|
||||||
|
|
||||||
# You can always kill the Stalfos' with the pots on easy/normal
|
# You can always kill the Stalfos' with the pots on easy/normal
|
||||||
if multiworld.enemy_health[player] in ("hard", "expert") or multiworld.enemy_shuffle[player]:
|
if world.options.enemy_health in ("hard", "expert") or world.options.enemy_shuffle:
|
||||||
stalfos_rule = lambda state: can_kill_most_things(state, player, 4)
|
stalfos_rule = lambda state: can_kill_most_things(state, player, 4)
|
||||||
for location in ['Eastern Palace - Compass Chest', 'Eastern Palace - Big Chest',
|
for location in ['Eastern Palace - Compass Chest', 'Eastern Palace - Big Chest',
|
||||||
'Eastern Palace - Dark Square Pot Key', 'Eastern Palace - Dark Eyegore Key Drop',
|
'Eastern Palace - Dark Square Pot Key', 'Eastern Palace - Dark Eyegore Key Drop',
|
||||||
@@ -362,14 +365,14 @@ def global_rules(multiworld: MultiWorld, player: int):
|
|||||||
add_rule(multiworld.get_location('Desert Palace - Boss', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player, 4) and state.has('Big Key (Desert Palace)', player) and has_fire_source(state, player) and state.multiworld.get_location('Desert Palace - Boss', player).parent_region.dungeon.boss.can_defeat(state))
|
add_rule(multiworld.get_location('Desert Palace - Boss', player), lambda state: state._lttp_has_key('Small Key (Desert Palace)', player, 4) and state.has('Big Key (Desert Palace)', player) and has_fire_source(state, player) and state.multiworld.get_location('Desert Palace - Boss', player).parent_region.dungeon.boss.can_defeat(state))
|
||||||
|
|
||||||
# logic patch to prevent placing a crystal in Desert that's required to reach the required keys
|
# logic patch to prevent placing a crystal in Desert that's required to reach the required keys
|
||||||
if not (multiworld.small_key_shuffle[player] and multiworld.big_key_shuffle[player]):
|
if not (world.options.small_key_shuffle and world.options.big_key_shuffle):
|
||||||
add_rule(multiworld.get_location('Desert Palace - Prize', player), lambda state: state.multiworld.get_region('Desert Palace Main (Outer)', player).can_reach(state))
|
add_rule(multiworld.get_location('Desert Palace - Prize', player), lambda state: state.multiworld.get_region('Desert Palace Main (Outer)', player).can_reach(state))
|
||||||
|
|
||||||
set_rule(multiworld.get_location('Tower of Hera - Basement Cage', player), lambda state: can_activate_crystal_switch(state, player))
|
set_rule(multiworld.get_location('Tower of Hera - Basement Cage', player), lambda state: can_activate_crystal_switch(state, player))
|
||||||
set_rule(multiworld.get_location('Tower of Hera - Map Chest', player), lambda state: can_activate_crystal_switch(state, player))
|
set_rule(multiworld.get_location('Tower of Hera - Map Chest', player), lambda state: can_activate_crystal_switch(state, player))
|
||||||
set_rule(multiworld.get_entrance('Tower of Hera Small Key Door', player), lambda state: can_activate_crystal_switch(state, player) and (state._lttp_has_key('Small Key (Tower of Hera)', player) or location_item_name(state, 'Tower of Hera - Big Key Chest', player) == ('Small Key (Tower of Hera)', player)))
|
set_rule(multiworld.get_entrance('Tower of Hera Small Key Door', player), lambda state: can_activate_crystal_switch(state, player) and (state._lttp_has_key('Small Key (Tower of Hera)', player) or location_item_name(state, 'Tower of Hera - Big Key Chest', player) == ('Small Key (Tower of Hera)', player)))
|
||||||
set_rule(multiworld.get_entrance('Tower of Hera Big Key Door', player), lambda state: can_activate_crystal_switch(state, player) and state.has('Big Key (Tower of Hera)', player))
|
set_rule(multiworld.get_entrance('Tower of Hera Big Key Door', player), lambda state: can_activate_crystal_switch(state, player) and state.has('Big Key (Tower of Hera)', player))
|
||||||
if multiworld.enemy_shuffle[player]:
|
if world.options.enemy_shuffle:
|
||||||
add_rule(multiworld.get_entrance('Tower of Hera Big Key Door', player), lambda state: can_kill_most_things(state, player, 3))
|
add_rule(multiworld.get_entrance('Tower of Hera Big Key Door', player), lambda state: can_kill_most_things(state, player, 3))
|
||||||
else:
|
else:
|
||||||
add_rule(multiworld.get_entrance('Tower of Hera Big Key Door', player),
|
add_rule(multiworld.get_entrance('Tower of Hera Big Key Door', player),
|
||||||
@@ -378,7 +381,7 @@ def global_rules(multiworld: MultiWorld, player: int):
|
|||||||
or state.has("Cane of Somaria", player)))
|
or state.has("Cane of Somaria", player)))
|
||||||
set_rule(multiworld.get_location('Tower of Hera - Big Chest', player), lambda state: state.has('Big Key (Tower of Hera)', player))
|
set_rule(multiworld.get_location('Tower of Hera - Big Chest', player), lambda state: state.has('Big Key (Tower of Hera)', player))
|
||||||
set_rule(multiworld.get_location('Tower of Hera - Big Key Chest', player), lambda state: has_fire_source(state, player))
|
set_rule(multiworld.get_location('Tower of Hera - Big Key Chest', player), lambda state: has_fire_source(state, player))
|
||||||
if multiworld.accessibility[player] != 'full':
|
if world.options.accessibility != 'full':
|
||||||
set_always_allow(multiworld.get_location('Tower of Hera - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Tower of Hera)' and item.player == player)
|
set_always_allow(multiworld.get_location('Tower of Hera - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Tower of Hera)' and item.player == player)
|
||||||
|
|
||||||
set_rule(multiworld.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Flippers', player) and state.has('Open Floodgate', player))
|
set_rule(multiworld.get_entrance('Swamp Palace Moat', player), lambda state: state.has('Flippers', player) and state.has('Open Floodgate', player))
|
||||||
@@ -387,32 +390,30 @@ def global_rules(multiworld: MultiWorld, player: int):
|
|||||||
set_rule(multiworld.get_location('Swamp Palace - Trench 1 Pot Key', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 2))
|
set_rule(multiworld.get_location('Swamp Palace - Trench 1 Pot Key', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 2))
|
||||||
set_rule(multiworld.get_entrance('Swamp Palace (Center)', player), lambda state: state.has('Hammer', player) and state._lttp_has_key('Small Key (Swamp Palace)', player, 3))
|
set_rule(multiworld.get_entrance('Swamp Palace (Center)', player), lambda state: state.has('Hammer', player) and state._lttp_has_key('Small Key (Swamp Palace)', player, 3))
|
||||||
set_rule(multiworld.get_location('Swamp Palace - Hookshot Pot Key', player), lambda state: state.has('Hookshot', player))
|
set_rule(multiworld.get_location('Swamp Palace - Hookshot Pot Key', player), lambda state: state.has('Hookshot', player))
|
||||||
if multiworld.pot_shuffle[player]:
|
if world.options.pot_shuffle:
|
||||||
# it could move the key to the top right platform which can only be reached with bombs
|
# it could move the key to the top right platform which can only be reached with bombs
|
||||||
add_rule(multiworld.get_location('Swamp Palace - Hookshot Pot Key', player), lambda state: can_use_bombs(state, player))
|
add_rule(multiworld.get_location('Swamp Palace - Hookshot Pot Key', player), lambda state: can_use_bombs(state, player))
|
||||||
set_rule(multiworld.get_entrance('Swamp Palace (West)', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 6)
|
set_rule(multiworld.get_entrance('Swamp Palace (West)', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 6))
|
||||||
if state.has('Hookshot', player)
|
|
||||||
else state._lttp_has_key('Small Key (Swamp Palace)', player, 4))
|
|
||||||
set_rule(multiworld.get_location('Swamp Palace - Big Chest', player), lambda state: state.has('Big Key (Swamp Palace)', player))
|
set_rule(multiworld.get_location('Swamp Palace - Big Chest', player), lambda state: state.has('Big Key (Swamp Palace)', player))
|
||||||
if multiworld.accessibility[player] != 'full':
|
if world.options.accessibility != 'full':
|
||||||
allow_self_locking_items(multiworld.get_location('Swamp Palace - Big Chest', player), 'Big Key (Swamp Palace)')
|
allow_self_locking_items(multiworld.get_location('Swamp Palace - Big Chest', player), 'Big Key (Swamp Palace)')
|
||||||
set_rule(multiworld.get_entrance('Swamp Palace (North)', player), lambda state: state.has('Hookshot', player) and state._lttp_has_key('Small Key (Swamp Palace)', player, 5))
|
set_rule(multiworld.get_entrance('Swamp Palace (North)', player), lambda state: state.has('Hookshot', player) and state._lttp_has_key('Small Key (Swamp Palace)', player, 5))
|
||||||
if not multiworld.small_key_shuffle[player] and multiworld.glitches_required[player] not in ['hybrid_major_glitches', 'no_logic']:
|
if not world.options.small_key_shuffle and world.options.glitches_required not in ['hybrid_major_glitches', 'no_logic']:
|
||||||
forbid_item(multiworld.get_location('Swamp Palace - Entrance', player), 'Big Key (Swamp Palace)', player)
|
forbid_item(multiworld.get_location('Swamp Palace - Entrance', player), 'Big Key (Swamp Palace)', player)
|
||||||
add_rule(multiworld.get_location('Swamp Palace - Prize', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 6))
|
add_rule(multiworld.get_location('Swamp Palace - Prize', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 6))
|
||||||
add_rule(multiworld.get_location('Swamp Palace - Boss', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 6))
|
add_rule(multiworld.get_location('Swamp Palace - Boss', player), lambda state: state._lttp_has_key('Small Key (Swamp Palace)', player, 6))
|
||||||
if multiworld.pot_shuffle[player]:
|
if world.options.pot_shuffle:
|
||||||
# key can (and probably will) be moved behind bombable wall
|
# key can (and probably will) be moved behind bombable wall
|
||||||
set_rule(multiworld.get_location('Swamp Palace - Waterway Pot Key', player), lambda state: can_use_bombs(state, player))
|
set_rule(multiworld.get_location('Swamp Palace - Waterway Pot Key', player), lambda state: can_use_bombs(state, player))
|
||||||
|
|
||||||
set_rule(multiworld.get_entrance('Thieves Town Big Key Door', player), lambda state: state.has('Big Key (Thieves Town)', player))
|
set_rule(multiworld.get_entrance('Thieves Town Big Key Door', player), lambda state: state.has('Big Key (Thieves Town)', player))
|
||||||
if multiworld.worlds[player].dungeons["Thieves Town"].boss.enemizer_name == "Blind":
|
if world.dungeons["Thieves Town"].boss.enemizer_name == "Blind":
|
||||||
set_rule(multiworld.get_entrance('Blind Fight', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player, 3) and can_use_bombs(state, player))
|
set_rule(multiworld.get_entrance('Blind Fight', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player, 3) and can_use_bombs(state, player))
|
||||||
set_rule(multiworld.get_location('Thieves\' Town - Big Chest', player),
|
set_rule(multiworld.get_location('Thieves\' Town - Big Chest', player),
|
||||||
lambda state: ((state._lttp_has_key('Small Key (Thieves Town)', player, 3)) or (location_item_name(state, 'Thieves\' Town - Big Chest', player) == ("Small Key (Thieves Town)", player)) and state._lttp_has_key('Small Key (Thieves Town)', player, 2)) and state.has('Hammer', player))
|
lambda state: ((state._lttp_has_key('Small Key (Thieves Town)', player, 3)) or (location_item_name(state, 'Thieves\' Town - Big Chest', player) == ("Small Key (Thieves Town)", player)) and state._lttp_has_key('Small Key (Thieves Town)', player, 2)) and state.has('Hammer', player))
|
||||||
set_rule(multiworld.get_location('Thieves\' Town - Blind\'s Cell', player),
|
set_rule(multiworld.get_location('Thieves\' Town - Blind\'s Cell', player),
|
||||||
lambda state: state._lttp_has_key('Small Key (Thieves Town)', player))
|
lambda state: state._lttp_has_key('Small Key (Thieves Town)', player))
|
||||||
if multiworld.accessibility[player] != 'full' and not multiworld.key_drop_shuffle[player]:
|
if world.options.accessibility != 'full' and not world.options.key_drop_shuffle:
|
||||||
set_always_allow(multiworld.get_location('Thieves\' Town - Big Chest', player), lambda state, item: item.name == 'Small Key (Thieves Town)' and item.player == player)
|
set_always_allow(multiworld.get_location('Thieves\' Town - Big Chest', player), lambda state, item: item.name == 'Small Key (Thieves Town)' and item.player == player)
|
||||||
set_rule(multiworld.get_location('Thieves\' Town - Attic', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player, 3))
|
set_rule(multiworld.get_location('Thieves\' Town - Attic', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player, 3))
|
||||||
set_rule(multiworld.get_location('Thieves\' Town - Spike Switch Pot Key', player),
|
set_rule(multiworld.get_location('Thieves\' Town - Spike Switch Pot Key', player),
|
||||||
@@ -424,7 +425,7 @@ def global_rules(multiworld: MultiWorld, player: int):
|
|||||||
set_rule(multiworld.get_entrance('Skull Woods First Section West Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5))
|
set_rule(multiworld.get_entrance('Skull Woods First Section West Door', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5))
|
||||||
set_rule(multiworld.get_entrance('Skull Woods First Section (Left) Door to Exit', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5))
|
set_rule(multiworld.get_entrance('Skull Woods First Section (Left) Door to Exit', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5))
|
||||||
set_rule(multiworld.get_location('Skull Woods - Big Chest', player), lambda state: state.has('Big Key (Skull Woods)', player) and can_use_bombs(state, player))
|
set_rule(multiworld.get_location('Skull Woods - Big Chest', player), lambda state: state.has('Big Key (Skull Woods)', player) and can_use_bombs(state, player))
|
||||||
if multiworld.accessibility[player] != 'full':
|
if world.options.accessibility != 'full':
|
||||||
allow_self_locking_items(multiworld.get_location('Skull Woods - Big Chest', player), 'Big Key (Skull Woods)')
|
allow_self_locking_items(multiworld.get_location('Skull Woods - Big Chest', player), 'Big Key (Skull Woods)')
|
||||||
set_rule(multiworld.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 4) and state.has('Fire Rod', player) and has_sword(state, player)) # sword required for curtain
|
set_rule(multiworld.get_entrance('Skull Woods Torch Room', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 4) and state.has('Fire Rod', player) and has_sword(state, player)) # sword required for curtain
|
||||||
add_rule(multiworld.get_location('Skull Woods - Prize', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5))
|
add_rule(multiworld.get_location('Skull Woods - Prize', player), lambda state: state._lttp_has_key('Small Key (Skull Woods)', player, 5))
|
||||||
@@ -501,13 +502,13 @@ def global_rules(multiworld: MultiWorld, player: int):
|
|||||||
set_rule(multiworld.get_entrance('Turtle Rock (Trinexx)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 6) and state.has('Big Key (Turtle Rock)', player) and state.has('Cane of Somaria', player))
|
set_rule(multiworld.get_entrance('Turtle Rock (Trinexx)', player), lambda state: state._lttp_has_key('Small Key (Turtle Rock)', player, 6) and state.has('Big Key (Turtle Rock)', player) and state.has('Cane of Somaria', player))
|
||||||
set_rule(multiworld.get_entrance('Turtle Rock Second Section Bomb Wall', player), lambda state: can_kill_most_things(state, player, 10))
|
set_rule(multiworld.get_entrance('Turtle Rock Second Section Bomb Wall', player), lambda state: can_kill_most_things(state, player, 10))
|
||||||
|
|
||||||
if not multiworld.worlds[player].fix_trock_doors:
|
if not world.fix_trock_doors:
|
||||||
add_rule(multiworld.get_entrance('Turtle Rock Second Section Bomb Wall', player), lambda state: can_use_bombs(state, player))
|
add_rule(multiworld.get_entrance('Turtle Rock Second Section Bomb Wall', player), lambda state: can_use_bombs(state, player))
|
||||||
set_rule(multiworld.get_entrance('Turtle Rock Second Section from Bomb Wall', player), lambda state: can_use_bombs(state, player))
|
set_rule(multiworld.get_entrance('Turtle Rock Second Section from Bomb Wall', player), lambda state: can_use_bombs(state, player))
|
||||||
set_rule(multiworld.get_entrance('Turtle Rock Eye Bridge from Bomb Wall', player), lambda state: can_use_bombs(state, player))
|
set_rule(multiworld.get_entrance('Turtle Rock Eye Bridge from Bomb Wall', player), lambda state: can_use_bombs(state, player))
|
||||||
set_rule(multiworld.get_entrance('Turtle Rock Eye Bridge Bomb Wall', player), lambda state: can_use_bombs(state, player))
|
set_rule(multiworld.get_entrance('Turtle Rock Eye Bridge Bomb Wall', player), lambda state: can_use_bombs(state, player))
|
||||||
|
|
||||||
if multiworld.enemy_shuffle[player]:
|
if world.options.enemy_shuffle:
|
||||||
set_rule(multiworld.get_entrance('Palace of Darkness Bonk Wall', player), lambda state: can_bomb_or_bonk(state, player) and can_kill_most_things(state, player, 3))
|
set_rule(multiworld.get_entrance('Palace of Darkness Bonk Wall', player), lambda state: can_bomb_or_bonk(state, player) and can_kill_most_things(state, player, 3))
|
||||||
else:
|
else:
|
||||||
set_rule(multiworld.get_entrance('Palace of Darkness Bonk Wall', player), lambda state: can_bomb_or_bonk(state, player) and can_shoot_arrows(state, player))
|
set_rule(multiworld.get_entrance('Palace of Darkness Bonk Wall', player), lambda state: can_bomb_or_bonk(state, player) and can_shoot_arrows(state, player))
|
||||||
@@ -517,18 +518,18 @@ def global_rules(multiworld: MultiWorld, player: int):
|
|||||||
set_rule(multiworld.get_entrance('Palace of Darkness (North)', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 4))
|
set_rule(multiworld.get_entrance('Palace of Darkness (North)', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 4))
|
||||||
set_rule(multiworld.get_location('Palace of Darkness - Big Chest', player), lambda state: can_use_bombs(state, player) and state.has('Big Key (Palace of Darkness)', player))
|
set_rule(multiworld.get_location('Palace of Darkness - Big Chest', player), lambda state: can_use_bombs(state, player) and state.has('Big Key (Palace of Darkness)', player))
|
||||||
set_rule(multiworld.get_location('Palace of Darkness - The Arena - Ledge', player), lambda state: can_use_bombs(state, player))
|
set_rule(multiworld.get_location('Palace of Darkness - The Arena - Ledge', player), lambda state: can_use_bombs(state, player))
|
||||||
if multiworld.pot_shuffle[player]:
|
if world.options.pot_shuffle:
|
||||||
# chest switch may be up on ledge where bombs are required
|
# chest switch may be up on ledge where bombs are required
|
||||||
set_rule(multiworld.get_location('Palace of Darkness - Stalfos Basement', player), lambda state: can_use_bombs(state, player))
|
set_rule(multiworld.get_location('Palace of Darkness - Stalfos Basement', player), lambda state: can_use_bombs(state, player))
|
||||||
|
|
||||||
set_rule(multiworld.get_entrance('Palace of Darkness Big Key Chest Staircase', player), lambda state: can_use_bombs(state, player) and (state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) or (
|
set_rule(multiworld.get_entrance('Palace of Darkness Big Key Chest Staircase', player), lambda state: can_use_bombs(state, player) and (state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) or (
|
||||||
location_item_name(state, 'Palace of Darkness - Big Key Chest', player) in [('Small Key (Palace of Darkness)', player)] and state._lttp_has_key('Small Key (Palace of Darkness)', player, 3))))
|
location_item_name(state, 'Palace of Darkness - Big Key Chest', player) in [('Small Key (Palace of Darkness)', player)] and state._lttp_has_key('Small Key (Palace of Darkness)', player, 3))))
|
||||||
if multiworld.accessibility[player] != 'full':
|
if world.options.accessibility != 'full':
|
||||||
set_always_allow(multiworld.get_location('Palace of Darkness - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state._lttp_has_key('Small Key (Palace of Darkness)', player, 5))
|
set_always_allow(multiworld.get_location('Palace of Darkness - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state._lttp_has_key('Small Key (Palace of Darkness)', player, 5))
|
||||||
|
|
||||||
set_rule(multiworld.get_entrance('Palace of Darkness Spike Statue Room Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) or (
|
set_rule(multiworld.get_entrance('Palace of Darkness Spike Statue Room Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6) or (
|
||||||
location_item_name(state, 'Palace of Darkness - Harmless Hellway', player) in [('Small Key (Palace of Darkness)', player)] and state._lttp_has_key('Small Key (Palace of Darkness)', player, 4)))
|
location_item_name(state, 'Palace of Darkness - Harmless Hellway', player) in [('Small Key (Palace of Darkness)', player)] and state._lttp_has_key('Small Key (Palace of Darkness)', player, 4)))
|
||||||
if multiworld.accessibility[player] != 'full':
|
if world.options.accessibility != 'full':
|
||||||
set_always_allow(multiworld.get_location('Palace of Darkness - Harmless Hellway', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state._lttp_has_key('Small Key (Palace of Darkness)', player, 5))
|
set_always_allow(multiworld.get_location('Palace of Darkness - Harmless Hellway', player), lambda state, item: item.name == 'Small Key (Palace of Darkness)' and item.player == player and state._lttp_has_key('Small Key (Palace of Darkness)', player, 5))
|
||||||
|
|
||||||
set_rule(multiworld.get_entrance('Palace of Darkness Maze Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6))
|
set_rule(multiworld.get_entrance('Palace of Darkness Maze Door', player), lambda state: state._lttp_has_key('Small Key (Palace of Darkness)', player, 6))
|
||||||
@@ -541,13 +542,13 @@ def global_rules(multiworld: MultiWorld, player: int):
|
|||||||
set_rule(multiworld.get_location('Ganons Tower - Bob\'s Torch', player), lambda state: state.has('Pegasus Boots', player))
|
set_rule(multiworld.get_location('Ganons Tower - Bob\'s Torch', player), lambda state: state.has('Pegasus Boots', player))
|
||||||
set_rule(multiworld.get_entrance('Ganons Tower (Tile Room)', player), lambda state: state.has('Cane of Somaria', player))
|
set_rule(multiworld.get_entrance('Ganons Tower (Tile Room)', player), lambda state: state.has('Cane of Somaria', player))
|
||||||
set_rule(multiworld.get_entrance('Ganons Tower (Hookshot Room)', player), lambda state: state.has('Hammer', player) and (state.has('Hookshot', player) or state.has('Pegasus Boots', player)))
|
set_rule(multiworld.get_entrance('Ganons Tower (Hookshot Room)', player), lambda state: state.has('Hammer', player) and (state.has('Hookshot', player) or state.has('Pegasus Boots', player)))
|
||||||
if multiworld.pot_shuffle[player]:
|
if world.options.pot_shuffle:
|
||||||
set_rule(multiworld.get_location('Ganons Tower - Conveyor Cross Pot Key', player), lambda state: state.has('Hammer', player) and (state.has('Hookshot', player) or state.has('Pegasus Boots', player)))
|
set_rule(multiworld.get_location('Ganons Tower - Conveyor Cross Pot Key', player), lambda state: state.has('Hammer', player) and (state.has('Hookshot', player) or state.has('Pegasus Boots', player)))
|
||||||
set_rule(multiworld.get_entrance('Ganons Tower (Map Room)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 8) or (
|
set_rule(multiworld.get_entrance('Ganons Tower (Map Room)', player), lambda state: state._lttp_has_key('Small Key (Ganons Tower)', player, 8) or (
|
||||||
location_item_name(state, 'Ganons Tower - Map Chest', player) in [('Big Key (Ganons Tower)', player)] and state._lttp_has_key('Small Key (Ganons Tower)', player, 6)))
|
location_item_name(state, 'Ganons Tower - Map Chest', player) in [('Big Key (Ganons Tower)', player)] and state._lttp_has_key('Small Key (Ganons Tower)', player, 6)))
|
||||||
|
|
||||||
# this seemed to be causing generation failure, disable for now
|
# this seemed to be causing generation failure, disable for now
|
||||||
# if world.accessibility[player] != 'full':
|
# if world.worlds[player].options.accessibility != 'full':
|
||||||
# set_always_allow(world.get_location('Ganons Tower - Map Chest', player), lambda state, item: item.name == 'Small Key (Ganons Tower)' and item.player == player and state._lttp_has_key('Small Key (Ganons Tower)', player, 7) and state.can_reach('Ganons Tower (Hookshot Room)', 'region', player))
|
# set_always_allow(world.get_location('Ganons Tower - Map Chest', player), lambda state, item: item.name == 'Small Key (Ganons Tower)' and item.player == player and state._lttp_has_key('Small Key (Ganons Tower)', player, 7) and state.can_reach('Ganons Tower (Hookshot Room)', 'region', player))
|
||||||
|
|
||||||
# It is possible to need more than 6 keys to get through this entrance if you spend keys elsewhere. We reflect this in the chest requirements.
|
# It is possible to need more than 6 keys to get through this entrance if you spend keys elsewhere. We reflect this in the chest requirements.
|
||||||
@@ -582,7 +583,7 @@ def global_rules(multiworld: MultiWorld, player: int):
|
|||||||
lambda state: can_use_bombs(state, player) and state.multiworld.get_location('Ganons Tower - Big Key Chest', player).parent_region.dungeon.bosses['bottom'].can_defeat(state))
|
lambda state: can_use_bombs(state, player) and state.multiworld.get_location('Ganons Tower - Big Key Chest', player).parent_region.dungeon.bosses['bottom'].can_defeat(state))
|
||||||
set_rule(multiworld.get_location('Ganons Tower - Big Key Room - Right', player),
|
set_rule(multiworld.get_location('Ganons Tower - Big Key Room - Right', player),
|
||||||
lambda state: can_use_bombs(state, player) and state.multiworld.get_location('Ganons Tower - Big Key Room - Right', player).parent_region.dungeon.bosses['bottom'].can_defeat(state))
|
lambda state: can_use_bombs(state, player) and state.multiworld.get_location('Ganons Tower - Big Key Room - Right', player).parent_region.dungeon.bosses['bottom'].can_defeat(state))
|
||||||
if multiworld.enemy_shuffle[player]:
|
if world.options.enemy_shuffle:
|
||||||
set_rule(multiworld.get_entrance('Ganons Tower Big Key Door', player),
|
set_rule(multiworld.get_entrance('Ganons Tower Big Key Door', player),
|
||||||
lambda state: state.has('Big Key (Ganons Tower)', player))
|
lambda state: state.has('Big Key (Ganons Tower)', player))
|
||||||
else:
|
else:
|
||||||
@@ -600,12 +601,12 @@ def global_rules(multiworld: MultiWorld, player: int):
|
|||||||
set_defeat_dungeon_boss_rule(multiworld.get_location('Agahnim 2', player))
|
set_defeat_dungeon_boss_rule(multiworld.get_location('Agahnim 2', player))
|
||||||
ganon = multiworld.get_location('Ganon', player)
|
ganon = multiworld.get_location('Ganon', player)
|
||||||
set_rule(ganon, lambda state: GanonDefeatRule(state, player))
|
set_rule(ganon, lambda state: GanonDefeatRule(state, player))
|
||||||
if multiworld.goal[player] in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']:
|
if world.options.goal in ['ganon_triforce_hunt', 'local_ganon_triforce_hunt']:
|
||||||
add_rule(ganon, lambda state: has_triforce_pieces(state, player))
|
add_rule(ganon, lambda state: has_triforce_pieces(state, player))
|
||||||
elif multiworld.goal[player] == 'ganon_pedestal':
|
elif world.options.goal == 'ganon_pedestal':
|
||||||
add_rule(multiworld.get_location('Ganon', player), lambda state: state.can_reach('Master Sword Pedestal', 'Location', player))
|
add_rule(multiworld.get_location('Ganon', player), lambda state: state.can_reach('Master Sword Pedestal', 'Location', player))
|
||||||
else:
|
else:
|
||||||
add_rule(ganon, lambda state: has_crystals(state, state.multiworld.crystals_needed_for_ganon[player], player))
|
add_rule(ganon, lambda state: has_crystals(state, state.multiworld.worlds[player].options.crystals_needed_for_ganon, player))
|
||||||
set_rule(multiworld.get_entrance('Ganon Drop', player), lambda state: has_beam_sword(state, player)) # need to damage ganon to get tiles to drop
|
set_rule(multiworld.get_entrance('Ganon Drop', player), lambda state: has_beam_sword(state, player)) # need to damage ganon to get tiles to drop
|
||||||
|
|
||||||
set_rule(multiworld.get_location('Flute Activation Spot', player), lambda state: state.has('Flute', player))
|
set_rule(multiworld.get_location('Flute Activation Spot', player), lambda state: state.has('Flute', player))
|
||||||
@@ -722,9 +723,9 @@ def default_rules(world, player):
|
|||||||
set_rule(world.get_entrance('Floating Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
|
set_rule(world.get_entrance('Floating Island Mirror Spot', player), lambda state: state.has('Magic Mirror', player))
|
||||||
set_rule(world.get_entrance('Turtle Rock', player), lambda state: state.has('Moon Pearl', player) and has_sword(state, player) and has_turtle_rock_medallion(state, player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword required to cast magic (!)
|
set_rule(world.get_entrance('Turtle Rock', player), lambda state: state.has('Moon Pearl', player) and has_sword(state, player) and has_turtle_rock_medallion(state, player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword required to cast magic (!)
|
||||||
|
|
||||||
set_rule(world.get_entrance('Pyramid Hole', player), lambda state: state.has('Beat Agahnim 2', player) or world.open_pyramid[player].to_bool(world, player))
|
set_rule(world.get_entrance('Pyramid Hole', player), lambda state: state.has('Beat Agahnim 2', player) or world.worlds[player].options.open_pyramid.to_bool(world, player))
|
||||||
|
|
||||||
if world.swordless[player]:
|
if world.worlds[player].options.swordless:
|
||||||
swordless_rules(world, player)
|
swordless_rules(world, player)
|
||||||
|
|
||||||
|
|
||||||
@@ -879,14 +880,14 @@ def inverted_rules(world, player):
|
|||||||
set_rule(world.get_entrance('Dark Grassy Lawn Flute', player), lambda state: state.has('Activated Flute', player))
|
set_rule(world.get_entrance('Dark Grassy Lawn Flute', player), lambda state: state.has('Activated Flute', player))
|
||||||
set_rule(world.get_entrance('Hammer Peg Area Flute', player), lambda state: state.has('Activated Flute', player))
|
set_rule(world.get_entrance('Hammer Peg Area Flute', player), lambda state: state.has('Activated Flute', player))
|
||||||
|
|
||||||
set_rule(world.get_entrance('Inverted Pyramid Hole', player), lambda state: state.has('Beat Agahnim 2', player) or world.open_pyramid[player])
|
set_rule(world.get_entrance('Inverted Pyramid Hole', player), lambda state: state.has('Beat Agahnim 2', player) or world.worlds[player].options.open_pyramid)
|
||||||
|
|
||||||
if world.swordless[player]:
|
if world.worlds[player].options.swordless:
|
||||||
swordless_rules(world, player)
|
swordless_rules(world, player)
|
||||||
|
|
||||||
def no_glitches_rules(world, player):
|
def no_glitches_rules(world, player):
|
||||||
""""""
|
""""""
|
||||||
if world.mode[player] == 'inverted':
|
if world.worlds[player].options.mode == 'inverted':
|
||||||
set_rule(world.get_entrance('Zoras River', player), lambda state: state.has('Moon Pearl', player) and (state.has('Flippers', player) or can_lift_rocks(state, player)))
|
set_rule(world.get_entrance('Zoras River', player), lambda state: state.has('Moon Pearl', player) and (state.has('Flippers', player) or can_lift_rocks(state, player)))
|
||||||
set_rule(world.get_entrance('Lake Hylia Central Island Pier', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # can be fake flippered to
|
set_rule(world.get_entrance('Lake Hylia Central Island Pier', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # can be fake flippered to
|
||||||
set_rule(world.get_entrance('Lake Hylia Island Pier', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # can be fake flippered to
|
set_rule(world.get_entrance('Lake Hylia Island Pier', player), lambda state: state.has('Moon Pearl', player) and state.has('Flippers', player)) # can be fake flippered to
|
||||||
@@ -910,7 +911,7 @@ def no_glitches_rules(world, player):
|
|||||||
add_conditional_lamps(world, player)
|
add_conditional_lamps(world, player)
|
||||||
|
|
||||||
def fake_flipper_rules(world, player):
|
def fake_flipper_rules(world, player):
|
||||||
if world.mode[player] == 'inverted':
|
if world.worlds[player].options.mode == 'inverted':
|
||||||
set_rule(world.get_entrance('Zoras River', player), lambda state: state.has('Moon Pearl', player))
|
set_rule(world.get_entrance('Zoras River', player), lambda state: state.has('Moon Pearl', player))
|
||||||
set_rule(world.get_entrance('Lake Hylia Central Island Pier', player), lambda state: state.has('Moon Pearl', player))
|
set_rule(world.get_entrance('Lake Hylia Central Island Pier', player), lambda state: state.has('Moon Pearl', player))
|
||||||
set_rule(world.get_entrance('Lake Hylia Island Pier', player), lambda state: state.has('Moon Pearl', player))
|
set_rule(world.get_entrance('Lake Hylia Island Pier', player), lambda state: state.has('Moon Pearl', player))
|
||||||
@@ -996,7 +997,7 @@ def add_conditional_lamps(world, player):
|
|||||||
'Location', True)
|
'Location', True)
|
||||||
add_conditional_lamp('Palace of Darkness - Dark Basement - Right', 'Palace of Darkness (Entrance)',
|
add_conditional_lamp('Palace of Darkness - Dark Basement - Right', 'Palace of Darkness (Entrance)',
|
||||||
'Location', True)
|
'Location', True)
|
||||||
if world.mode[player] != 'inverted':
|
if world.worlds[player].options.mode != 'inverted':
|
||||||
add_conditional_lamp('Agahnim 1', 'Agahnims Tower', 'Entrance')
|
add_conditional_lamp('Agahnim 1', 'Agahnims Tower', 'Entrance')
|
||||||
add_conditional_lamp('Castle Tower - Dark Maze', 'Agahnims Tower')
|
add_conditional_lamp('Castle Tower - Dark Maze', 'Agahnims Tower')
|
||||||
add_conditional_lamp('Castle Tower - Dark Archer Key Drop', 'Agahnims Tower')
|
add_conditional_lamp('Castle Tower - Dark Archer Key Drop', 'Agahnims Tower')
|
||||||
@@ -1018,7 +1019,7 @@ def add_conditional_lamps(world, player):
|
|||||||
add_conditional_lamp('Eastern Palace - Boss', 'Eastern Palace', 'Location', True)
|
add_conditional_lamp('Eastern Palace - Boss', 'Eastern Palace', 'Location', True)
|
||||||
add_conditional_lamp('Eastern Palace - Prize', 'Eastern Palace', 'Location', True)
|
add_conditional_lamp('Eastern Palace - Prize', 'Eastern Palace', 'Location', True)
|
||||||
|
|
||||||
if not world.mode[player] == "standard":
|
if not world.worlds[player].options.mode == "standard":
|
||||||
add_lamp_requirement(world, world.get_location('Sewers - Dark Cross', player), player)
|
add_lamp_requirement(world, world.get_location('Sewers - Dark Cross', player), player)
|
||||||
add_lamp_requirement(world, world.get_entrance('Sewers Back Door', player), player)
|
add_lamp_requirement(world, world.get_entrance('Sewers Back Door', player), player)
|
||||||
add_lamp_requirement(world, world.get_entrance('Throne Room', player), player)
|
add_lamp_requirement(world, world.get_entrance('Throne Room', player), player)
|
||||||
@@ -1044,7 +1045,7 @@ def open_rules(world, player):
|
|||||||
set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player),
|
set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player),
|
||||||
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 4)
|
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 4)
|
||||||
and state.has('Big Key (Hyrule Castle)', player)
|
and state.has('Big Key (Hyrule Castle)', player)
|
||||||
and (world.enemy_health[player] in ("easy", "default")
|
and (world.worlds[player].options.enemy_health in ("easy", "default")
|
||||||
or can_kill_most_things(state, player, 1)))
|
or can_kill_most_things(state, player, 1)))
|
||||||
|
|
||||||
|
|
||||||
@@ -1058,7 +1059,7 @@ def swordless_rules(world, player):
|
|||||||
|
|
||||||
set_rule(world.get_entrance('Ganon Drop', player), lambda state: state.has('Hammer', player)) # need to damage ganon to get tiles to drop
|
set_rule(world.get_entrance('Ganon Drop', player), lambda state: state.has('Hammer', player)) # need to damage ganon to get tiles to drop
|
||||||
|
|
||||||
if world.mode[player] != 'inverted':
|
if world.worlds[player].options.mode != 'inverted':
|
||||||
set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has('Cape', player) or state.has('Hammer', player) or state.has('Beat Agahnim 1', player)) # barrier gets removed after killing agahnim, relevant for entrance shuffle
|
set_rule(world.get_entrance('Agahnims Tower', player), lambda state: state.has('Cape', player) or state.has('Hammer', player) or state.has('Beat Agahnim 1', player)) # barrier gets removed after killing agahnim, relevant for entrance shuffle
|
||||||
set_rule(world.get_entrance('Turtle Rock', player), lambda state: state.has('Moon Pearl', player) and has_turtle_rock_medallion(state, player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword not required to use medallion for opening in swordless (!)
|
set_rule(world.get_entrance('Turtle Rock', player), lambda state: state.has('Moon Pearl', player) and has_turtle_rock_medallion(state, player) and state.can_reach('Turtle Rock (Top)', 'Region', player)) # sword not required to use medallion for opening in swordless (!)
|
||||||
set_rule(world.get_entrance('Misery Mire', player), lambda state: state.has('Moon Pearl', player) and has_misery_mire_medallion(state, player)) # sword not required to use medallion for opening in swordless (!)
|
set_rule(world.get_entrance('Misery Mire', player), lambda state: state.has('Moon Pearl', player) and has_misery_mire_medallion(state, player)) # sword not required to use medallion for opening in swordless (!)
|
||||||
@@ -1071,9 +1072,8 @@ def swordless_rules(world, player):
|
|||||||
def add_connection(parent_name, target_name, entrance_name, world, player):
|
def add_connection(parent_name, target_name, entrance_name, world, player):
|
||||||
parent = world.get_region(parent_name, player)
|
parent = world.get_region(parent_name, player)
|
||||||
target = world.get_region(target_name, player)
|
target = world.get_region(target_name, player)
|
||||||
connection = Entrance(player, entrance_name, parent)
|
parent.connect(target, entrance_name)
|
||||||
parent.exits.append(connection)
|
|
||||||
connection.connect(target)
|
|
||||||
|
|
||||||
|
|
||||||
def standard_rules(world, player):
|
def standard_rules(world, player):
|
||||||
@@ -1085,7 +1085,7 @@ def standard_rules(world, player):
|
|||||||
set_rule(world.get_entrance('Links House S&Q', player), lambda state: state.can_reach('Sanctuary', 'Region', player))
|
set_rule(world.get_entrance('Links House S&Q', player), lambda state: state.can_reach('Sanctuary', 'Region', player))
|
||||||
set_rule(world.get_entrance('Sanctuary S&Q', player), lambda state: state.can_reach('Sanctuary', 'Region', player))
|
set_rule(world.get_entrance('Sanctuary S&Q', player), lambda state: state.can_reach('Sanctuary', 'Region', player))
|
||||||
|
|
||||||
if world.small_key_shuffle[player] != small_key_shuffle.option_universal:
|
if world.worlds[player].options.small_key_shuffle != small_key_shuffle.option_universal:
|
||||||
set_rule(world.get_location('Hyrule Castle - Boomerang Guard Key Drop', player),
|
set_rule(world.get_location('Hyrule Castle - Boomerang Guard Key Drop', player),
|
||||||
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 1)
|
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 1)
|
||||||
and can_kill_most_things(state, player, 2))
|
and can_kill_most_things(state, player, 2))
|
||||||
@@ -1098,7 +1098,7 @@ def standard_rules(world, player):
|
|||||||
set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player),
|
set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player),
|
||||||
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 2)
|
lambda state: state._lttp_has_key('Small Key (Hyrule Castle)', player, 2)
|
||||||
and state.has('Big Key (Hyrule Castle)', player)
|
and state.has('Big Key (Hyrule Castle)', player)
|
||||||
and (world.enemy_health[player] in ("easy", "default")
|
and (world.worlds[player].options.enemy_health in ("easy", "default")
|
||||||
or can_kill_most_things(state, player, 1)))
|
or can_kill_most_things(state, player, 1)))
|
||||||
|
|
||||||
set_rule(world.get_location('Sewers - Key Rat Key Drop', player),
|
set_rule(world.get_location('Sewers - Key Rat Key Drop', player),
|
||||||
@@ -1108,6 +1108,7 @@ def standard_rules(world, player):
|
|||||||
set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player),
|
set_rule(world.get_location('Hyrule Castle - Zelda\'s Chest', player),
|
||||||
lambda state: state.has('Big Key (Hyrule Castle)', player))
|
lambda state: state.has('Big Key (Hyrule Castle)', player))
|
||||||
|
|
||||||
|
|
||||||
def toss_junk_item(world, player):
|
def toss_junk_item(world, player):
|
||||||
items = ['Rupees (20)', 'Bombs (3)', 'Arrows (10)', 'Rupees (5)', 'Rupee (1)', 'Bombs (10)',
|
items = ['Rupees (20)', 'Bombs (3)', 'Arrows (10)', 'Rupees (5)', 'Rupee (1)', 'Bombs (10)',
|
||||||
'Single Arrow', 'Rupees (50)', 'Rupees (100)', 'Single Bomb', 'Bee', 'Bee Trap',
|
'Single Arrow', 'Rupees (50)', 'Rupees (100)', 'Single Bomb', 'Bee', 'Bee Trap',
|
||||||
@@ -1195,15 +1196,15 @@ def set_trock_key_rules(multiworld, player):
|
|||||||
return 6
|
return 6
|
||||||
|
|
||||||
# If TR is only accessible from the middle, the big key must be further restricted to prevent softlock potential
|
# If TR is only accessible from the middle, the big key must be further restricted to prevent softlock potential
|
||||||
if not can_reach_front and not multiworld.small_key_shuffle[player]:
|
if not can_reach_front and not multiworld.worlds[player].options.small_key_shuffle:
|
||||||
# Must not go in the Big Key Chest - only 1 other chest available and 2+ keys required for all other chests
|
# Must not go in the Big Key Chest - only 1 other chest available and 2+ keys required for all other chests
|
||||||
forbid_item(multiworld.get_location('Turtle Rock - Big Key Chest', player), 'Big Key (Turtle Rock)', player)
|
forbid_item(multiworld.get_location('Turtle Rock - Big Key Chest', player), 'Big Key (Turtle Rock)', player)
|
||||||
if not can_reach_big_chest:
|
if not can_reach_big_chest:
|
||||||
# Must not go in the Chain Chomps chest - only 2 other chests available and 3+ keys required for all other chests
|
# Must not go in the Chain Chomps chest - only 2 other chests available and 3+ keys required for all other chests
|
||||||
forbid_item(multiworld.get_location('Turtle Rock - Chain Chomps', player), 'Big Key (Turtle Rock)', player)
|
forbid_item(multiworld.get_location('Turtle Rock - Chain Chomps', player), 'Big Key (Turtle Rock)', player)
|
||||||
forbid_item(multiworld.get_location('Turtle Rock - Pokey 2 Key Drop', player), 'Big Key (Turtle Rock)', player)
|
forbid_item(multiworld.get_location('Turtle Rock - Pokey 2 Key Drop', player), 'Big Key (Turtle Rock)', player)
|
||||||
if multiworld.accessibility[player] == 'full':
|
if multiworld.worlds[player].options.accessibility == 'full':
|
||||||
if multiworld.big_key_shuffle[player] and can_reach_big_chest:
|
if multiworld.worlds[player].options.big_key_shuffle and can_reach_big_chest:
|
||||||
# Must not go in the dungeon - all 3 available chests (Chomps, Big Chest, Crystaroller) must be keys to access laser bridge, and the big key is required first
|
# Must not go in the dungeon - all 3 available chests (Chomps, Big Chest, Crystaroller) must be keys to access laser bridge, and the big key is required first
|
||||||
for location in ['Turtle Rock - Chain Chomps', 'Turtle Rock - Compass Chest',
|
for location in ['Turtle Rock - Chain Chomps', 'Turtle Rock - Compass Chest',
|
||||||
'Turtle Rock - Pokey 1 Key Drop', 'Turtle Rock - Pokey 2 Key Drop',
|
'Turtle Rock - Pokey 1 Key Drop', 'Turtle Rock - Pokey 2 Key Drop',
|
||||||
@@ -1216,9 +1217,9 @@ def set_trock_key_rules(multiworld, player):
|
|||||||
location.place_locked_item(item)
|
location.place_locked_item(item)
|
||||||
toss_junk_item(multiworld, player)
|
toss_junk_item(multiworld, player)
|
||||||
|
|
||||||
if multiworld.accessibility[player] != 'full':
|
if multiworld.worlds[player].options.accessibility != 'full':
|
||||||
set_always_allow(multiworld.get_location('Turtle Rock - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Turtle Rock)' and item.player == player
|
set_always_allow(multiworld.get_location('Turtle Rock - Big Key Chest', player), lambda state, item: item.name == 'Small Key (Turtle Rock)' and item.player == player
|
||||||
and state.can_reach(state.multiworld.get_region('Turtle Rock (Second Section)', player)))
|
and state.can_reach(state.multiworld.get_region('Turtle Rock (Second Section)', player)))
|
||||||
|
|
||||||
|
|
||||||
def set_big_bomb_rules(world, player):
|
def set_big_bomb_rules(world, player):
|
||||||
@@ -1683,7 +1684,7 @@ def set_bunny_rules(world: MultiWorld, player: int, inverted: bool):
|
|||||||
def get_rule_to_add(region, location = None, connecting_entrance = None):
|
def get_rule_to_add(region, location = None, connecting_entrance = None):
|
||||||
# In OWG, a location can potentially be superbunny-mirror accessible or
|
# In OWG, a location can potentially be superbunny-mirror accessible or
|
||||||
# bunny revival accessible.
|
# bunny revival accessible.
|
||||||
if world.glitches_required[player] in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic']:
|
if world.worlds[player].options.glitches_required in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic']:
|
||||||
if region.name == 'Swamp Palace (Entrance)': # Need to 0hp revive - not in logic
|
if region.name == 'Swamp Palace (Entrance)': # Need to 0hp revive - not in logic
|
||||||
return lambda state: state.has('Moon Pearl', player)
|
return lambda state: state.has('Moon Pearl', player)
|
||||||
if region.name == 'Tower of Hera (Bottom)': # Need to hit the crystal switch
|
if region.name == 'Tower of Hera (Bottom)': # Need to hit the crystal switch
|
||||||
@@ -1723,7 +1724,7 @@ def set_bunny_rules(world: MultiWorld, player: int, inverted: bool):
|
|||||||
seen.add(new_region)
|
seen.add(new_region)
|
||||||
if not is_link(new_region):
|
if not is_link(new_region):
|
||||||
# For glitch rulesets, establish superbunny and revival rules.
|
# For glitch rulesets, establish superbunny and revival rules.
|
||||||
if world.glitches_required[player] in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic'] and entrance.name not in OverworldGlitchRules.get_invalid_bunny_revival_dungeons():
|
if world.worlds[player].options.glitches_required in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic'] and entrance.name not in OverworldGlitchRules.get_invalid_bunny_revival_dungeons():
|
||||||
if region.name in OverworldGlitchRules.get_sword_required_superbunny_mirror_regions():
|
if region.name in OverworldGlitchRules.get_sword_required_superbunny_mirror_regions():
|
||||||
possible_options.append(lambda state: path_to_access_rule(new_path, entrance) and state.has('Magic Mirror', player) and has_sword(state, player))
|
possible_options.append(lambda state: path_to_access_rule(new_path, entrance) and state.has('Magic Mirror', player) and has_sword(state, player))
|
||||||
elif (region.name in OverworldGlitchRules.get_boots_required_superbunny_mirror_regions()
|
elif (region.name in OverworldGlitchRules.get_boots_required_superbunny_mirror_regions()
|
||||||
@@ -1760,7 +1761,7 @@ def set_bunny_rules(world: MultiWorld, player: int, inverted: bool):
|
|||||||
# Add requirements for all locations that are actually in the dark world, except those available to the bunny, including dungeon revival
|
# Add requirements for all locations that are actually in the dark world, except those available to the bunny, including dungeon revival
|
||||||
for entrance in world.get_entrances(player):
|
for entrance in world.get_entrances(player):
|
||||||
if is_bunny(entrance.connected_region):
|
if is_bunny(entrance.connected_region):
|
||||||
if world.glitches_required[player] in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic'] :
|
if world.worlds[player].options.glitches_required in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic'] :
|
||||||
if entrance.connected_region.type == LTTPRegionType.Dungeon:
|
if entrance.connected_region.type == LTTPRegionType.Dungeon:
|
||||||
if entrance.parent_region.type != LTTPRegionType.Dungeon and entrance.connected_region.name in OverworldGlitchRules.get_invalid_bunny_revival_dungeons():
|
if entrance.parent_region.type != LTTPRegionType.Dungeon and entrance.connected_region.name in OverworldGlitchRules.get_invalid_bunny_revival_dungeons():
|
||||||
add_rule(entrance, get_rule_to_add(entrance.connected_region, None, entrance))
|
add_rule(entrance, get_rule_to_add(entrance.connected_region, None, entrance))
|
||||||
@@ -1768,7 +1769,7 @@ def set_bunny_rules(world: MultiWorld, player: int, inverted: bool):
|
|||||||
if entrance.connected_region.name == 'Turtle Rock (Entrance)':
|
if entrance.connected_region.name == 'Turtle Rock (Entrance)':
|
||||||
add_rule(world.get_entrance('Turtle Rock Entrance Gap', player), get_rule_to_add(entrance.connected_region, None, entrance))
|
add_rule(world.get_entrance('Turtle Rock Entrance Gap', player), get_rule_to_add(entrance.connected_region, None, entrance))
|
||||||
for location in entrance.connected_region.locations:
|
for location in entrance.connected_region.locations:
|
||||||
if world.glitches_required[player] in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic'] and entrance.name in OverworldGlitchRules.get_invalid_mirror_bunny_entrances():
|
if world.worlds[player].options.glitches_required in ['minor_glitches', 'overworld_glitches', 'hybrid_major_glitches', 'no_logic'] and entrance.name in OverworldGlitchRules.get_invalid_mirror_bunny_entrances():
|
||||||
continue
|
continue
|
||||||
if location.name in bunny_accessible_locations:
|
if location.name in bunny_accessible_locations:
|
||||||
continue
|
continue
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ def push_shop_inventories(multiworld):
|
|||||||
for location in shop_slots:
|
for location in shop_slots:
|
||||||
item_name = location.item.name
|
item_name = location.item.name
|
||||||
# Retro Bow arrows will already have been pushed
|
# Retro Bow arrows will already have been pushed
|
||||||
if (not multiworld.retro_bow[location.player]) or ((item_name, location.item.player)
|
if (not multiworld.worlds[location.player].options.retro_bow) or ((item_name, location.item.player)
|
||||||
!= ("Single Arrow", location.player)):
|
!= ("Single Arrow", location.player)):
|
||||||
location.shop.push_inventory(location.shop_slot, item_name,
|
location.shop.push_inventory(location.shop_slot, item_name,
|
||||||
round(location.shop_price * get_price_modifier(location.item)),
|
round(location.shop_price * get_price_modifier(location.item)),
|
||||||
@@ -185,36 +185,36 @@ def push_shop_inventories(multiworld):
|
|||||||
def create_shops(multiworld, player: int):
|
def create_shops(multiworld, player: int):
|
||||||
from .Options import RandomizeShopInventories
|
from .Options import RandomizeShopInventories
|
||||||
player_shop_table = shop_table.copy()
|
player_shop_table = shop_table.copy()
|
||||||
if multiworld.include_witch_hut[player]:
|
if multiworld.worlds[player].options.include_witch_hut:
|
||||||
player_shop_table["Potion Shop"] = player_shop_table["Potion Shop"]._replace(locked=False)
|
player_shop_table["Potion Shop"] = player_shop_table["Potion Shop"]._replace(locked=False)
|
||||||
dynamic_shop_slots = total_dynamic_shop_slots + 3
|
dynamic_shop_slots = total_dynamic_shop_slots + 3
|
||||||
else:
|
else:
|
||||||
dynamic_shop_slots = total_dynamic_shop_slots
|
dynamic_shop_slots = total_dynamic_shop_slots
|
||||||
if multiworld.shuffle_capacity_upgrades[player]:
|
if multiworld.worlds[player].options.shuffle_capacity_upgrades:
|
||||||
player_shop_table["Capacity Upgrade"] = player_shop_table["Capacity Upgrade"]._replace(locked=False)
|
player_shop_table["Capacity Upgrade"] = player_shop_table["Capacity Upgrade"]._replace(locked=False)
|
||||||
|
|
||||||
num_slots = min(dynamic_shop_slots, multiworld.shop_item_slots[player])
|
num_slots = min(dynamic_shop_slots, multiworld.worlds[player].options.shop_item_slots)
|
||||||
single_purchase_slots: List[bool] = [True] * num_slots + [False] * (dynamic_shop_slots - num_slots)
|
single_purchase_slots: List[bool] = [True] * num_slots + [False] * (dynamic_shop_slots - num_slots)
|
||||||
multiworld.random.shuffle(single_purchase_slots)
|
multiworld.random.shuffle(single_purchase_slots)
|
||||||
|
|
||||||
if multiworld.randomize_shop_inventories[player]:
|
if multiworld.worlds[player].options.randomize_shop_inventories:
|
||||||
default_shop_table = [i for l in
|
default_shop_table = [i for l in
|
||||||
[shop_generation_types[x] for x in ['arrows', 'bombs', 'potions', 'shields', 'bottle'] if
|
[shop_generation_types[x] for x in ['arrows', 'bombs', 'potions', 'shields', 'bottle'] if
|
||||||
not multiworld.retro_bow[player] or x != 'arrows'] for i in l]
|
not multiworld.worlds[player].options.retro_bow or x != 'arrows'] for i in l]
|
||||||
new_basic_shop = multiworld.random.sample(default_shop_table, k=3)
|
new_basic_shop = multiworld.random.sample(default_shop_table, k=3)
|
||||||
new_dark_shop = multiworld.random.sample(default_shop_table, k=3)
|
new_dark_shop = multiworld.random.sample(default_shop_table, k=3)
|
||||||
for name, shop in player_shop_table.items():
|
for name, shop in player_shop_table.items():
|
||||||
typ, shop_id, keeper, custom, locked, items, sram_offset = shop
|
typ, shop_id, keeper, custom, locked, items, sram_offset = shop
|
||||||
if not locked:
|
if not locked:
|
||||||
new_items = multiworld.random.sample(default_shop_table, k=len(items))
|
new_items = multiworld.random.sample(default_shop_table, k=len(items))
|
||||||
if multiworld.randomize_shop_inventories[player] == RandomizeShopInventories.option_randomize_by_shop_type:
|
if multiworld.worlds[player].options.randomize_shop_inventories == RandomizeShopInventories.option_randomize_by_shop_type:
|
||||||
if items == _basic_shop_defaults:
|
if items == _basic_shop_defaults:
|
||||||
new_items = new_basic_shop
|
new_items = new_basic_shop
|
||||||
elif items == _dark_world_shop_defaults:
|
elif items == _dark_world_shop_defaults:
|
||||||
new_items = new_dark_shop
|
new_items = new_dark_shop
|
||||||
keeper = multiworld.random.choice([0xA0, 0xC1, 0xFF])
|
keeper = multiworld.random.choice([0xA0, 0xC1, 0xFF])
|
||||||
player_shop_table[name] = ShopData(typ, shop_id, keeper, custom, locked, new_items, sram_offset)
|
player_shop_table[name] = ShopData(typ, shop_id, keeper, custom, locked, new_items, sram_offset)
|
||||||
if multiworld.mode[player] == "inverted":
|
if multiworld.worlds[player].options.mode == "inverted":
|
||||||
# make sure that blue potion is available in inverted, special case locked = None; lock when done.
|
# make sure that blue potion is available in inverted, special case locked = None; lock when done.
|
||||||
player_shop_table["Dark Lake Hylia Shop"] = \
|
player_shop_table["Dark Lake Hylia Shop"] = \
|
||||||
player_shop_table["Dark Lake Hylia Shop"]._replace(items=_inverted_hylia_shop_defaults, locked=None)
|
player_shop_table["Dark Lake Hylia Shop"]._replace(items=_inverted_hylia_shop_defaults, locked=None)
|
||||||
@@ -237,7 +237,7 @@ def create_shops(multiworld, player: int):
|
|||||||
add_rule(loc, lambda state, spot=loc: shop_price_rules(state, player, spot))
|
add_rule(loc, lambda state, spot=loc: shop_price_rules(state, player, spot))
|
||||||
loc.shop = shop
|
loc.shop = shop
|
||||||
loc.shop_slot = index
|
loc.shop_slot = index
|
||||||
if ((not (multiworld.shuffle_capacity_upgrades[player] and type == ShopType.UpgradeShop))
|
if ((not (multiworld.worlds[player].options.shuffle_capacity_upgrades and type == ShopType.UpgradeShop))
|
||||||
and not single_purchase_slots.pop()):
|
and not single_purchase_slots.pop()):
|
||||||
loc.shop_slot_disabled = True
|
loc.shop_slot_disabled = True
|
||||||
loc.locked = True
|
loc.locked = True
|
||||||
@@ -309,18 +309,18 @@ def set_up_shops(multiworld, player: int):
|
|||||||
from .Options import small_key_shuffle
|
from .Options import small_key_shuffle
|
||||||
# TODO: move hard+ mode changes for shields here, utilizing the new shops
|
# TODO: move hard+ mode changes for shields here, utilizing the new shops
|
||||||
|
|
||||||
if multiworld.retro_bow[player]:
|
if multiworld.worlds[player].options.retro_bow:
|
||||||
rss = multiworld.get_region('Red Shield Shop', player).shop
|
rss = multiworld.get_region('Red Shield Shop', player).shop
|
||||||
replacement_items = [['Red Potion', 150], ['Green Potion', 75], ['Blue Potion', 200], ['Bombs (10)', 50],
|
replacement_items = [['Red Potion', 150], ['Green Potion', 75], ['Blue Potion', 200], ['Bombs (10)', 50],
|
||||||
['Blue Shield', 50], ['Small Heart',
|
['Blue Shield', 50], ['Small Heart',
|
||||||
10]] # Can't just replace the single arrow with 10 arrows as retro doesn't need them.
|
10]] # Can't just replace the single arrow with 10 arrows as retro doesn't need them.
|
||||||
if multiworld.small_key_shuffle[player] == small_key_shuffle.option_universal:
|
if multiworld.worlds[player].options.small_key_shuffle == small_key_shuffle.option_universal:
|
||||||
replacement_items.append(['Small Key (Universal)', 100])
|
replacement_items.append(['Small Key (Universal)', 100])
|
||||||
replacement_item = multiworld.random.choice(replacement_items)
|
replacement_item = multiworld.random.choice(replacement_items)
|
||||||
rss.add_inventory(2, 'Single Arrow', 80, 1, replacement_item[0], replacement_item[1])
|
rss.add_inventory(2, 'Single Arrow', 80, 1, replacement_item[0], replacement_item[1])
|
||||||
rss.locked = True
|
rss.locked = True
|
||||||
|
|
||||||
if multiworld.small_key_shuffle[player] == small_key_shuffle.option_universal or multiworld.retro_bow[player]:
|
if multiworld.worlds[player].options.small_key_shuffle == small_key_shuffle.option_universal or multiworld.worlds[player].options.retro_bow:
|
||||||
for shop in multiworld.random.sample([s for s in multiworld.shops if
|
for shop in multiworld.random.sample([s for s in multiworld.shops if
|
||||||
s.custom and not s.locked and s.type == ShopType.Shop
|
s.custom and not s.locked and s.type == ShopType.Shop
|
||||||
and s.region.player == player], 5):
|
and s.region.player == player], 5):
|
||||||
@@ -328,19 +328,19 @@ def set_up_shops(multiworld, player: int):
|
|||||||
slots = [0, 1, 2]
|
slots = [0, 1, 2]
|
||||||
multiworld.random.shuffle(slots)
|
multiworld.random.shuffle(slots)
|
||||||
slots = iter(slots)
|
slots = iter(slots)
|
||||||
if multiworld.small_key_shuffle[player] == small_key_shuffle.option_universal:
|
if multiworld.worlds[player].options.small_key_shuffle == small_key_shuffle.option_universal:
|
||||||
shop.add_inventory(next(slots), 'Small Key (Universal)', 100)
|
shop.add_inventory(next(slots), 'Small Key (Universal)', 100)
|
||||||
if multiworld.retro_bow[player]:
|
if multiworld.worlds[player].options.retro_bow:
|
||||||
shop.push_inventory(next(slots), 'Single Arrow', 80)
|
shop.push_inventory(next(slots), 'Single Arrow', 80)
|
||||||
|
|
||||||
if multiworld.shuffle_capacity_upgrades[player]:
|
if multiworld.worlds[player].options.shuffle_capacity_upgrades:
|
||||||
for shop in multiworld.shops:
|
for shop in multiworld.shops:
|
||||||
if shop.type == ShopType.UpgradeShop and shop.region.player == player and \
|
if shop.type == ShopType.UpgradeShop and shop.region.player == player and \
|
||||||
shop.region.name == "Capacity Upgrade":
|
shop.region.name == "Capacity Upgrade":
|
||||||
shop.clear_inventory()
|
shop.clear_inventory()
|
||||||
|
|
||||||
if (multiworld.shuffle_shop_inventories[player] or multiworld.randomize_shop_prices[player]
|
if (multiworld.worlds[player].options.shuffle_shop_inventories or multiworld.worlds[player].options.randomize_shop_prices
|
||||||
or multiworld.randomize_cost_types[player]):
|
or multiworld.worlds[player].options.randomize_cost_types):
|
||||||
shops = []
|
shops = []
|
||||||
total_inventory = []
|
total_inventory = []
|
||||||
for shop in multiworld.shops:
|
for shop in multiworld.shops:
|
||||||
@@ -352,7 +352,7 @@ def set_up_shops(multiworld, player: int):
|
|||||||
for item in total_inventory:
|
for item in total_inventory:
|
||||||
item["price_type"], item["price"] = get_price(multiworld, item, player)
|
item["price_type"], item["price"] = get_price(multiworld, item, player)
|
||||||
|
|
||||||
if multiworld.shuffle_shop_inventories[player]:
|
if multiworld.worlds[player].options.shuffle_shop_inventories:
|
||||||
multiworld.random.shuffle(total_inventory)
|
multiworld.random.shuffle(total_inventory)
|
||||||
|
|
||||||
i = 0
|
i = 0
|
||||||
@@ -434,39 +434,39 @@ def get_price(multiworld, item, player: int, price_type=None):
|
|||||||
price_types = [price_type]
|
price_types = [price_type]
|
||||||
else:
|
else:
|
||||||
price_types = [ShopPriceType.Rupees] # included as a chance to not change price
|
price_types = [ShopPriceType.Rupees] # included as a chance to not change price
|
||||||
if multiworld.randomize_cost_types[player]:
|
if multiworld.worlds[player].options.randomize_cost_types:
|
||||||
price_types += [
|
price_types += [
|
||||||
ShopPriceType.Hearts,
|
ShopPriceType.Hearts,
|
||||||
ShopPriceType.Bombs,
|
ShopPriceType.Bombs,
|
||||||
ShopPriceType.Magic,
|
ShopPriceType.Magic,
|
||||||
]
|
]
|
||||||
if multiworld.small_key_shuffle[player] == small_key_shuffle.option_universal:
|
if multiworld.worlds[player].options.small_key_shuffle == small_key_shuffle.option_universal:
|
||||||
if item and item["item"] == "Small Key (Universal)":
|
if item and item["item"] == "Small Key (Universal)":
|
||||||
price_types = [ShopPriceType.Rupees, ShopPriceType.Magic] # no logical requirements for repeatable keys
|
price_types = [ShopPriceType.Rupees, ShopPriceType.Magic] # no logical requirements for repeatable keys
|
||||||
else:
|
else:
|
||||||
price_types.append(ShopPriceType.Keys)
|
price_types.append(ShopPriceType.Keys)
|
||||||
if multiworld.retro_bow[player]:
|
if multiworld.worlds[player].options.retro_bow:
|
||||||
if item and item["item"] == "Single Arrow":
|
if item and item["item"] == "Single Arrow":
|
||||||
price_types = [ShopPriceType.Rupees, ShopPriceType.Magic] # no logical requirements for arrows
|
price_types = [ShopPriceType.Rupees, ShopPriceType.Magic] # no logical requirements for arrows
|
||||||
else:
|
else:
|
||||||
price_types.append(ShopPriceType.Arrows)
|
price_types.append(ShopPriceType.Arrows)
|
||||||
diff = multiworld.item_pool[player].value
|
diff = multiworld.worlds[player].options.item_pool.value
|
||||||
if item:
|
if item:
|
||||||
# This is for a shop's regular inventory, the item is already determined, and we will decide the price here
|
# This is for a shop's regular inventory, the item is already determined, and we will decide the price here
|
||||||
price = item["price"]
|
price = item["price"]
|
||||||
if multiworld.randomize_shop_prices[player]:
|
if multiworld.worlds[player].options.randomize_shop_prices:
|
||||||
adjust = 2 if price < 100 else 5
|
adjust = 2 if price < 100 else 5
|
||||||
price = int((price / adjust) * (0.5 + multiworld.per_slot_randoms[player].random() * 1.5)) * adjust
|
price = int((price / adjust) * (0.5 + multiworld.worlds[player].random.random() * 1.5)) * adjust
|
||||||
multiworld.per_slot_randoms[player].shuffle(price_types)
|
multiworld.worlds[player].random.shuffle(price_types)
|
||||||
for p_type in price_types:
|
for p_type in price_types:
|
||||||
if any(x in item['item'] for x in price_blacklist[p_type]):
|
if any(x in item['item'] for x in price_blacklist[p_type]):
|
||||||
continue
|
continue
|
||||||
return p_type, price_chart[p_type](price, diff)
|
return p_type, price_chart[p_type](price, diff)
|
||||||
else:
|
else:
|
||||||
# This is an AP location and the price will be adjusted after an item is shuffled into it
|
# This is an AP location and the price will be adjusted after an item is shuffled into it
|
||||||
p_type = multiworld.per_slot_randoms[player].choice(price_types)
|
p_type = multiworld.worlds[player].random.choice(price_types)
|
||||||
return p_type, price_chart[p_type](min(int(multiworld.per_slot_randoms[player].randint(8, 56)
|
return p_type, price_chart[p_type](min(int(multiworld.worlds[player].random.randint(8, 56)
|
||||||
* multiworld.shop_price_modifier[player] / 100) * 5, 9999), diff)
|
* multiworld.worlds[player].options.shop_price_modifier / 100) * 5, 9999), diff)
|
||||||
|
|
||||||
|
|
||||||
def shop_price_rules(state: CollectionState, player: int, location: ALttPLocation):
|
def shop_price_rules(state: CollectionState, player: int, location: ALttPLocation):
|
||||||
|
|||||||
@@ -6,7 +6,7 @@ def is_not_bunny(state: CollectionState, region: LTTPRegion, player: int) -> boo
|
|||||||
if state.has('Moon Pearl', player):
|
if state.has('Moon Pearl', player):
|
||||||
return True
|
return True
|
||||||
|
|
||||||
return region.is_light_world if state.multiworld.mode[player] != 'inverted' else region.is_dark_world
|
return region.is_light_world if state.multiworld.worlds[player].options.mode != 'inverted' else region.is_dark_world
|
||||||
|
|
||||||
|
|
||||||
def can_bomb_clip(state: CollectionState, region: LTTPRegion, player: int) -> bool:
|
def can_bomb_clip(state: CollectionState, region: LTTPRegion, player: int) -> bool:
|
||||||
@@ -24,7 +24,7 @@ def can_buy(state: CollectionState, item: str, player: int) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def can_shoot_arrows(state: CollectionState, player: int, count: int = 0) -> bool:
|
def can_shoot_arrows(state: CollectionState, player: int, count: int = 0) -> bool:
|
||||||
if state.multiworld.retro_bow[player]:
|
if state.multiworld.worlds[player].options.retro_bow:
|
||||||
return (state.has('Bow', player) or state.has('Silver Bow', player)) and can_buy(state, 'Single Arrow', player)
|
return (state.has('Bow', player) or state.has('Silver Bow', player)) and can_buy(state, 'Single Arrow', player)
|
||||||
return (state.has('Bow', player) or state.has('Silver Bow', player)) and can_hold_arrows(state, player, count)
|
return (state.has('Bow', player) or state.has('Silver Bow', player)) and can_hold_arrows(state, player, count)
|
||||||
|
|
||||||
@@ -74,9 +74,9 @@ def can_extend_magic(state: CollectionState, player: int, smallmagic: int = 16,
|
|||||||
elif state.has('Magic Upgrade (1/2)', player):
|
elif state.has('Magic Upgrade (1/2)', player):
|
||||||
basemagic = 16
|
basemagic = 16
|
||||||
if can_buy_unlimited(state, 'Green Potion', player) or can_buy_unlimited(state, 'Blue Potion', player):
|
if can_buy_unlimited(state, 'Green Potion', player) or can_buy_unlimited(state, 'Blue Potion', player):
|
||||||
if state.multiworld.item_functionality[player] == 'hard' and not fullrefill:
|
if state.multiworld.worlds[player].options.item_functionality == 'hard' and not fullrefill:
|
||||||
basemagic = basemagic + int(basemagic * 0.5 * bottle_count(state, player))
|
basemagic = basemagic + int(basemagic * 0.5 * bottle_count(state, player))
|
||||||
elif state.multiworld.item_functionality[player] == 'expert' and not fullrefill:
|
elif state.multiworld.worlds[player].options.item_functionality == 'expert' and not fullrefill:
|
||||||
basemagic = basemagic + int(basemagic * 0.25 * bottle_count(state, player))
|
basemagic = basemagic + int(basemagic * 0.25 * bottle_count(state, player))
|
||||||
else:
|
else:
|
||||||
basemagic = basemagic + basemagic * bottle_count(state, player)
|
basemagic = basemagic + basemagic * bottle_count(state, player)
|
||||||
@@ -99,12 +99,12 @@ def can_hold_arrows(state: CollectionState, player: int, quantity: int):
|
|||||||
|
|
||||||
|
|
||||||
def can_use_bombs(state: CollectionState, player: int, quantity: int = 1) -> bool:
|
def can_use_bombs(state: CollectionState, player: int, quantity: int = 1) -> bool:
|
||||||
bombs = 0 if state.multiworld.bombless_start[player] else 10
|
bombs = 0 if state.multiworld.worlds[player].options.bombless_start else 10
|
||||||
bombs += ((state.count("Bomb Upgrade (+5)", player) * 5) + (state.count("Bomb Upgrade (+10)", player) * 10)
|
bombs += ((state.count("Bomb Upgrade (+5)", player) * 5) + (state.count("Bomb Upgrade (+10)", player) * 10)
|
||||||
+ (state.count("Bomb Upgrade (50)", player) * 50))
|
+ (state.count("Bomb Upgrade (50)", player) * 50))
|
||||||
# Bomb Upgrade (+5) beyond the 6th gives +10
|
# Bomb Upgrade (+5) beyond the 6th gives +10
|
||||||
bombs += max(0, ((state.count("Bomb Upgrade (+5)", player) - 6) * 10))
|
bombs += max(0, ((state.count("Bomb Upgrade (+5)", player) - 6) * 10))
|
||||||
if (not state.multiworld.shuffle_capacity_upgrades[player]) and state.has("Capacity Upgrade Shop", player):
|
if (not state.multiworld.worlds[player].options.shuffle_capacity_upgrades) and state.has("Capacity Upgrade Shop", player):
|
||||||
bombs += 40
|
bombs += 40
|
||||||
return bombs >= min(quantity, 50)
|
return bombs >= min(quantity, 50)
|
||||||
|
|
||||||
@@ -120,7 +120,7 @@ def can_activate_crystal_switch(state: CollectionState, player: int) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def can_kill_most_things(state: CollectionState, player: int, enemies: int = 5) -> bool:
|
def can_kill_most_things(state: CollectionState, player: int, enemies: int = 5) -> bool:
|
||||||
if state.multiworld.enemy_shuffle[player]:
|
if state.multiworld.worlds[player].options.enemy_shuffle:
|
||||||
# I don't fully understand Enemizer's logic for placing enemies in spots where they need to be killable, if any.
|
# I don't fully understand Enemizer's logic for placing enemies in spots where they need to be killable, if any.
|
||||||
# Just go with maximal requirements for now.
|
# Just go with maximal requirements for now.
|
||||||
return (has_melee_weapon(state, player)
|
return (has_melee_weapon(state, player)
|
||||||
@@ -135,7 +135,7 @@ def can_kill_most_things(state: CollectionState, player: int, enemies: int = 5)
|
|||||||
or (state.has('Cane of Byrna', player) and (enemies < 6 or can_extend_magic(state, player)))
|
or (state.has('Cane of Byrna', player) and (enemies < 6 or can_extend_magic(state, player)))
|
||||||
or can_shoot_arrows(state, player)
|
or can_shoot_arrows(state, player)
|
||||||
or state.has('Fire Rod', player)
|
or state.has('Fire Rod', player)
|
||||||
or (state.multiworld.enemy_health[player] in ("easy", "default")
|
or (state.multiworld.worlds[player].options.enemy_health in ("easy", "default")
|
||||||
and can_use_bombs(state, player, enemies * 4)))
|
and can_use_bombs(state, player, enemies * 4)))
|
||||||
|
|
||||||
|
|
||||||
@@ -152,7 +152,7 @@ def can_get_good_bee(state: CollectionState, player: int) -> bool:
|
|||||||
|
|
||||||
def can_retrieve_tablet(state: CollectionState, player: int) -> bool:
|
def can_retrieve_tablet(state: CollectionState, player: int) -> bool:
|
||||||
return state.has('Book of Mudora', player) and (has_beam_sword(state, player) or
|
return state.has('Book of Mudora', player) and (has_beam_sword(state, player) or
|
||||||
(state.multiworld.swordless[player] and
|
(state.multiworld.worlds[player].options.swordless and
|
||||||
state.has("Hammer", player)))
|
state.has("Hammer", player)))
|
||||||
|
|
||||||
|
|
||||||
@@ -179,7 +179,7 @@ def has_fire_source(state: CollectionState, player: int) -> bool:
|
|||||||
def can_melt_things(state: CollectionState, player: int) -> bool:
|
def can_melt_things(state: CollectionState, player: int) -> bool:
|
||||||
return state.has('Fire Rod', player) or \
|
return state.has('Fire Rod', player) or \
|
||||||
(state.has('Bombos', player) and
|
(state.has('Bombos', player) and
|
||||||
(state.multiworld.swordless[player] or
|
(state.multiworld.worlds[player].options.swordless or
|
||||||
has_sword(state, player)))
|
has_sword(state, player)))
|
||||||
|
|
||||||
|
|
||||||
@@ -192,19 +192,19 @@ def has_turtle_rock_medallion(state: CollectionState, player: int) -> bool:
|
|||||||
|
|
||||||
|
|
||||||
def can_boots_clip_lw(state: CollectionState, player: int) -> bool:
|
def can_boots_clip_lw(state: CollectionState, player: int) -> bool:
|
||||||
if state.multiworld.mode[player] == 'inverted':
|
if state.multiworld.worlds[player].options.mode == 'inverted':
|
||||||
return state.has('Pegasus Boots', player) and state.has('Moon Pearl', player)
|
return state.has('Pegasus Boots', player) and state.has('Moon Pearl', player)
|
||||||
return state.has('Pegasus Boots', player)
|
return state.has('Pegasus Boots', player)
|
||||||
|
|
||||||
|
|
||||||
def can_boots_clip_dw(state: CollectionState, player: int) -> bool:
|
def can_boots_clip_dw(state: CollectionState, player: int) -> bool:
|
||||||
if state.multiworld.mode[player] != 'inverted':
|
if state.multiworld.worlds[player].options.mode != 'inverted':
|
||||||
return state.has('Pegasus Boots', player) and state.has('Moon Pearl', player)
|
return state.has('Pegasus Boots', player) and state.has('Moon Pearl', player)
|
||||||
return state.has('Pegasus Boots', player)
|
return state.has('Pegasus Boots', player)
|
||||||
|
|
||||||
|
|
||||||
def can_get_glitched_speed_dw(state: CollectionState, player: int) -> bool:
|
def can_get_glitched_speed_dw(state: CollectionState, player: int) -> bool:
|
||||||
rules = [state.has('Pegasus Boots', player), any([state.has('Hookshot', player), has_sword(state, player)])]
|
rules = [state.has('Pegasus Boots', player), any([state.has('Hookshot', player), has_sword(state, player)])]
|
||||||
if state.multiworld.mode[player] != 'inverted':
|
if state.multiworld.worlds[player].options.mode != 'inverted':
|
||||||
rules.append(state.has('Moon Pearl', player))
|
rules.append(state.has('Moon Pearl', player))
|
||||||
return all(rules)
|
return all(rules)
|
||||||
|
|||||||
@@ -2,11 +2,10 @@
|
|||||||
from typing import Optional, TYPE_CHECKING
|
from typing import Optional, TYPE_CHECKING
|
||||||
from enum import IntEnum
|
from enum import IntEnum
|
||||||
|
|
||||||
from BaseClasses import Location, Item, ItemClassification, Region, MultiWorld
|
from BaseClasses import Entrance, Location, Item, ItemClassification, Region, MultiWorld
|
||||||
|
|
||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from .Dungeons import Dungeon
|
from .Dungeons import Dungeon
|
||||||
from .Regions import LTTPRegion
|
|
||||||
|
|
||||||
|
|
||||||
class ALttPLocation(Location):
|
class ALttPLocation(Location):
|
||||||
@@ -77,6 +76,19 @@ class ALttPItem(Item):
|
|||||||
return self.type
|
return self.type
|
||||||
|
|
||||||
|
|
||||||
|
Addresses = int | list[int] | tuple[int, int, int, int, int, int, int, int, int, int, int, int, int]
|
||||||
|
|
||||||
|
|
||||||
|
class LTTPEntrance(Entrance):
|
||||||
|
addresses: Addresses | None = None
|
||||||
|
target: int | None = None
|
||||||
|
|
||||||
|
def connect(self, region: Region, addresses: Addresses | None = None, target: int | None = None) -> None:
|
||||||
|
super().connect(region)
|
||||||
|
self.addresses = addresses
|
||||||
|
self.target = target
|
||||||
|
|
||||||
|
|
||||||
class LTTPRegionType(IntEnum):
|
class LTTPRegionType(IntEnum):
|
||||||
LightWorld = 1
|
LightWorld = 1
|
||||||
DarkWorld = 2
|
DarkWorld = 2
|
||||||
@@ -90,6 +102,7 @@ class LTTPRegionType(IntEnum):
|
|||||||
|
|
||||||
|
|
||||||
class LTTPRegion(Region):
|
class LTTPRegion(Region):
|
||||||
|
entrance_type = LTTPEntrance
|
||||||
type: LTTPRegionType
|
type: LTTPRegionType
|
||||||
|
|
||||||
# will be set after making connections.
|
# will be set after making connections.
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
from BaseClasses import Entrance
|
|
||||||
from worlds.generic.Rules import set_rule, add_rule
|
from worlds.generic.Rules import set_rule, add_rule
|
||||||
from .StateHelpers import can_bomb_clip, has_sword, has_beam_sword, has_fire_source, can_melt_things, has_misery_mire_medallion
|
from .StateHelpers import can_bomb_clip, has_sword, has_beam_sword, has_fire_source, can_melt_things, has_misery_mire_medallion
|
||||||
|
from .SubClasses import LTTPEntrance
|
||||||
|
|
||||||
|
|
||||||
# We actually need the logic to properly "mark" these regions as Light or Dark world.
|
# We actually need the logic to properly "mark" these regions as Light or Dark world.
|
||||||
@@ -9,11 +9,9 @@ def underworld_glitch_connections(world, player):
|
|||||||
specrock = world.get_region('Spectacle Rock Cave (Bottom)', player)
|
specrock = world.get_region('Spectacle Rock Cave (Bottom)', player)
|
||||||
mire = world.get_region('Misery Mire (West)', player)
|
mire = world.get_region('Misery Mire (West)', player)
|
||||||
|
|
||||||
kikiskip = Entrance(player, 'Kiki Skip', specrock)
|
kikiskip = specrock.create_exit('Kiki Skip')
|
||||||
mire_to_hera = Entrance(player, 'Mire to Hera Clip', mire)
|
mire_to_hera = mire.create_exit('Mire to Hera Clip')
|
||||||
mire_to_swamp = Entrance(player, 'Hera to Swamp Clip', mire)
|
mire_to_swamp = mire.create_exit('Hera to Swamp Clip')
|
||||||
specrock.exits.append(kikiskip)
|
|
||||||
mire.exits.extend([mire_to_hera, mire_to_swamp])
|
|
||||||
|
|
||||||
if world.worlds[player].fix_fake_world:
|
if world.worlds[player].fix_fake_world:
|
||||||
kikiskip.connect(world.get_entrance('Palace of Darkness Exit', player).connected_region)
|
kikiskip.connect(world.get_entrance('Palace of Darkness Exit', player).connected_region)
|
||||||
@@ -37,7 +35,7 @@ def fake_pearl_state(state, player):
|
|||||||
|
|
||||||
# Sets the rules on where we can actually go using this clip.
|
# Sets the rules on where we can actually go using this clip.
|
||||||
# Behavior differs based on what type of ER shuffle we're playing.
|
# Behavior differs based on what type of ER shuffle we're playing.
|
||||||
def dungeon_reentry_rules(world, player, clip: Entrance, dungeon_region: str, dungeon_exit: str):
|
def dungeon_reentry_rules(world, player, clip: LTTPEntrance, dungeon_region: str, dungeon_exit: str):
|
||||||
fix_dungeon_exits = world.worlds[player].fix_palaceofdarkness_exit
|
fix_dungeon_exits = world.worlds[player].fix_palaceofdarkness_exit
|
||||||
fix_fake_worlds = world.worlds[player].fix_fake_world
|
fix_fake_worlds = world.worlds[player].fix_fake_world
|
||||||
|
|
||||||
@@ -90,12 +88,12 @@ def underworld_glitches_rules(world, player):
|
|||||||
# We need to be able to s+q to old man, then go to either Mire or Hera at either Hera or GT.
|
# We need to be able to s+q to old man, then go to either Mire or Hera at either Hera or GT.
|
||||||
# First we require a certain type of entrance shuffle, then build the rule from its pieces.
|
# First we require a certain type of entrance shuffle, then build the rule from its pieces.
|
||||||
if not world.worlds[player].swamp_patch_required:
|
if not world.worlds[player].swamp_patch_required:
|
||||||
if world.entrance_shuffle[player] in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
|
if world.worlds[player].options.entrance_shuffle in ['vanilla', 'dungeons_simple', 'dungeons_full', 'dungeons_crossed']:
|
||||||
rule_map = {
|
rule_map = {
|
||||||
'Misery Mire (Entrance)': (lambda state: True),
|
'Misery Mire (Entrance)': (lambda state: True),
|
||||||
'Tower of Hera (Bottom)': (lambda state: state.can_reach('Tower of Hera Big Key Door', 'Entrance', player))
|
'Tower of Hera (Bottom)': (lambda state: state.can_reach('Tower of Hera Big Key Door', 'Entrance', player))
|
||||||
}
|
}
|
||||||
inverted = world.mode[player] == 'inverted'
|
inverted = world.worlds[player].options.mode == 'inverted'
|
||||||
hera_rule = lambda state: (state.has('Moon Pearl', player) or not inverted) and \
|
hera_rule = lambda state: (state.has('Moon Pearl', player) or not inverted) and \
|
||||||
rule_map.get(world.get_entrance('Tower of Hera', player).connected_region.name, lambda state: False)(state)
|
rule_map.get(world.get_entrance('Tower of Hera', player).connected_region.name, lambda state: False)(state)
|
||||||
gt_rule = lambda state: (state.has('Moon Pearl', player) or inverted) and \
|
gt_rule = lambda state: (state.has('Moon Pearl', player) or inverted) and \
|
||||||
|
|||||||
@@ -141,7 +141,7 @@ class ALTTPWorld(World):
|
|||||||
item_name_groups = item_name_groups
|
item_name_groups = item_name_groups
|
||||||
location_name_groups = {
|
location_name_groups = {
|
||||||
"Blind's Hideout": {"Blind's Hideout - Top", "Blind's Hideout - Left", "Blind's Hideout - Right",
|
"Blind's Hideout": {"Blind's Hideout - Top", "Blind's Hideout - Left", "Blind's Hideout - Right",
|
||||||
"Blind's Hideout - Far Left", "Blind's Hideout - Far Right"},
|
"Blind's Hideout - Far Left", "Blind's Hideout - Far Right"},
|
||||||
"Kakariko Well": {"Kakariko Well - Top", "Kakariko Well - Left", "Kakariko Well - Middle",
|
"Kakariko Well": {"Kakariko Well - Top", "Kakariko Well - Left", "Kakariko Well - Middle",
|
||||||
"Kakariko Well - Right", "Kakariko Well - Bottom"},
|
"Kakariko Well - Right", "Kakariko Well - Bottom"},
|
||||||
"Mini Moldorm Cave": {"Mini Moldorm Cave - Far Left", "Mini Moldorm Cave - Left", "Mini Moldorm Cave - Right",
|
"Mini Moldorm Cave": {"Mini Moldorm Cave - Far Left", "Mini Moldorm Cave - Left", "Mini Moldorm Cave - Right",
|
||||||
@@ -154,15 +154,23 @@ class ALTTPWorld(World):
|
|||||||
"Hookshot Cave": {"Hookshot Cave - Top Right", "Hookshot Cave - Top Left", "Hookshot Cave - Bottom Right",
|
"Hookshot Cave": {"Hookshot Cave - Top Right", "Hookshot Cave - Top Left", "Hookshot Cave - Bottom Right",
|
||||||
"Hookshot Cave - Bottom Left"},
|
"Hookshot Cave - Bottom Left"},
|
||||||
"Hyrule Castle": {"Hyrule Castle - Boomerang Chest", "Hyrule Castle - Map Chest",
|
"Hyrule Castle": {"Hyrule Castle - Boomerang Chest", "Hyrule Castle - Map Chest",
|
||||||
"Hyrule Castle - Zelda's Chest", "Sewers - Dark Cross", "Sewers - Secret Room - Left",
|
"Hyrule Castle - Zelda's Chest", "Hyrule Castle - Big Key Drop",
|
||||||
"Sewers - Secret Room - Middle", "Sewers - Secret Room - Right"},
|
"Hyrule Castle - Boomerang Guard Key Drop", "Hyrule Castle - Map Guard Key Drop",
|
||||||
|
"Sewers - Dark Cross", "Sewers - Secret Room - Left",
|
||||||
|
"Sewers - Secret Room - Middle", "Sewers - Secret Room - Right",
|
||||||
|
"Sewers - Key Rat Key Drop"},
|
||||||
"Eastern Palace": {"Eastern Palace - Compass Chest", "Eastern Palace - Big Chest",
|
"Eastern Palace": {"Eastern Palace - Compass Chest", "Eastern Palace - Big Chest",
|
||||||
"Eastern Palace - Cannonball Chest", "Eastern Palace - Big Key Chest",
|
"Eastern Palace - Cannonball Chest", "Eastern Palace - Big Key Chest",
|
||||||
|
"Eastern Palace - Dark Eyegore Key Drop", "Eastern Palace - Dark Square Pot Key",
|
||||||
"Eastern Palace - Map Chest", "Eastern Palace - Boss"},
|
"Eastern Palace - Map Chest", "Eastern Palace - Boss"},
|
||||||
"Desert Palace": {"Desert Palace - Big Chest", "Desert Palace - Torch", "Desert Palace - Map Chest",
|
"Desert Palace": {"Desert Palace - Big Chest", "Desert Palace - Torch", "Desert Palace - Map Chest",
|
||||||
"Desert Palace - Compass Chest", "Desert Palace - Big Key Chest", "Desert Palace - Boss"},
|
"Desert Palace - Beamos Hall Pot Key", "Desert Palace - Desert Tiles 1 Pot Key",
|
||||||
|
"Desert Palace - Desert Tiles 2 Pot Key", "Desert Palace - Compass Chest",
|
||||||
|
"Desert Palace - Big Key Chest", "Desert Palace - Boss"},
|
||||||
"Tower of Hera": {"Tower of Hera - Basement Cage", "Tower of Hera - Map Chest", "Tower of Hera - Big Key Chest",
|
"Tower of Hera": {"Tower of Hera - Basement Cage", "Tower of Hera - Map Chest", "Tower of Hera - Big Key Chest",
|
||||||
"Tower of Hera - Compass Chest", "Tower of Hera - Big Chest", "Tower of Hera - Boss"},
|
"Tower of Hera - Compass Chest", "Tower of Hera - Big Chest", "Tower of Hera - Boss"},
|
||||||
|
"Castle Tower": {"Castle Tower - Room 03", "Castle Tower - Dark Maze",
|
||||||
|
"Castle Tower - Dark Archer Key Drop", "Castle Tower - Circle of Pots Key Drop"},
|
||||||
"Palace of Darkness": {"Palace of Darkness - Shooter Room", "Palace of Darkness - The Arena - Bridge",
|
"Palace of Darkness": {"Palace of Darkness - Shooter Room", "Palace of Darkness - The Arena - Bridge",
|
||||||
"Palace of Darkness - Stalfos Basement", "Palace of Darkness - Big Key Chest",
|
"Palace of Darkness - Stalfos Basement", "Palace of Darkness - Big Key Chest",
|
||||||
"Palace of Darkness - The Arena - Ledge", "Palace of Darkness - Map Chest",
|
"Palace of Darkness - The Arena - Ledge", "Palace of Darkness - Map Chest",
|
||||||
@@ -173,25 +181,33 @@ class ALTTPWorld(World):
|
|||||||
"Swamp Palace": {"Swamp Palace - Entrance", "Swamp Palace - Map Chest", "Swamp Palace - Big Chest",
|
"Swamp Palace": {"Swamp Palace - Entrance", "Swamp Palace - Map Chest", "Swamp Palace - Big Chest",
|
||||||
"Swamp Palace - Compass Chest", "Swamp Palace - Big Key Chest", "Swamp Palace - West Chest",
|
"Swamp Palace - Compass Chest", "Swamp Palace - Big Key Chest", "Swamp Palace - West Chest",
|
||||||
"Swamp Palace - Flooded Room - Left", "Swamp Palace - Flooded Room - Right",
|
"Swamp Palace - Flooded Room - Left", "Swamp Palace - Flooded Room - Right",
|
||||||
"Swamp Palace - Waterfall Room", "Swamp Palace - Boss"},
|
"Swamp Palace - Hookshot Pot Key", "Swamp Palace - Pot Row Pot Key",
|
||||||
|
"Swamp Palace - Trench 1 Pot Key", "Swamp Palace - Trench 2 Pot Key",
|
||||||
|
"Swamp Palace - Waterway Pot Key", "Swamp Palace - Waterfall Room", "Swamp Palace - Boss"},
|
||||||
"Thieves' Town": {"Thieves' Town - Big Key Chest", "Thieves' Town - Map Chest", "Thieves' Town - Compass Chest",
|
"Thieves' Town": {"Thieves' Town - Big Key Chest", "Thieves' Town - Map Chest", "Thieves' Town - Compass Chest",
|
||||||
"Thieves' Town - Ambush Chest", "Thieves' Town - Attic", "Thieves' Town - Big Chest",
|
"Thieves' Town - Ambush Chest", "Thieves' Town - Attic", "Thieves' Town - Big Chest",
|
||||||
|
"Thieves' Town - Hallway Pot Key", "Thieves' Town - Spike Switch Pot Key",
|
||||||
"Thieves' Town - Blind's Cell", "Thieves' Town - Boss"},
|
"Thieves' Town - Blind's Cell", "Thieves' Town - Boss"},
|
||||||
"Skull Woods": {"Skull Woods - Map Chest", "Skull Woods - Pinball Room", "Skull Woods - Compass Chest",
|
"Skull Woods": {"Skull Woods - Map Chest", "Skull Woods - Pinball Room", "Skull Woods - Compass Chest",
|
||||||
"Skull Woods - Pot Prison", "Skull Woods - Big Chest", "Skull Woods - Big Key Chest",
|
"Skull Woods - Pot Prison", "Skull Woods - Big Chest", "Skull Woods - Big Key Chest",
|
||||||
|
"Skull Woods - Spike Corner Key Drop", "Skull Woods - West Lobby Pot Key",
|
||||||
"Skull Woods - Bridge Room", "Skull Woods - Boss"},
|
"Skull Woods - Bridge Room", "Skull Woods - Boss"},
|
||||||
"Ice Palace": {"Ice Palace - Compass Chest", "Ice Palace - Freezor Chest", "Ice Palace - Big Chest",
|
"Ice Palace": {"Ice Palace - Compass Chest", "Ice Palace - Freezor Chest", "Ice Palace - Big Chest",
|
||||||
"Ice Palace - Freezor Chest", "Ice Palace - Big Chest", "Ice Palace - Iced T Room",
|
"Ice Palace - Freezor Chest", "Ice Palace - Big Chest", "Ice Palace - Iced T Room",
|
||||||
"Ice Palace - Spike Room", "Ice Palace - Big Key Chest", "Ice Palace - Map Chest",
|
"Ice Palace - Spike Room", "Ice Palace - Big Key Chest", "Ice Palace - Map Chest",
|
||||||
|
"Ice Palace - Conveyor Key Drop", "Ice Palace - Hammer Block Key Drop",
|
||||||
|
"Ice Palace - Jelly Key Drop", "Ice Palace - Many Pots Pot Key",
|
||||||
"Ice Palace - Boss"},
|
"Ice Palace - Boss"},
|
||||||
"Misery Mire": {"Misery Mire - Big Chest", "Misery Mire - Map Chest", "Misery Mire - Main Lobby",
|
"Misery Mire": {"Misery Mire - Big Chest", "Misery Mire - Map Chest", "Misery Mire - Main Lobby",
|
||||||
"Misery Mire - Bridge Chest", "Misery Mire - Spike Chest", "Misery Mire - Compass Chest",
|
"Misery Mire - Bridge Chest", "Misery Mire - Spike Chest", "Misery Mire - Compass Chest",
|
||||||
"Misery Mire - Big Key Chest", "Misery Mire - Boss"},
|
"Misery Mire - Conveyor Crystal Key Drop", "Misery Mire - Fishbone Pot Key",
|
||||||
|
"Misery Mire - Spikes Pot Key", "Misery Mire - Big Key Chest", "Misery Mire - Boss"},
|
||||||
"Turtle Rock": {"Turtle Rock - Compass Chest", "Turtle Rock - Roller Room - Left",
|
"Turtle Rock": {"Turtle Rock - Compass Chest", "Turtle Rock - Roller Room - Left",
|
||||||
"Turtle Rock - Roller Room - Right", "Turtle Rock - Chain Chomps", "Turtle Rock - Big Key Chest",
|
"Turtle Rock - Roller Room - Right", "Turtle Rock - Chain Chomps", "Turtle Rock - Big Key Chest",
|
||||||
"Turtle Rock - Big Chest", "Turtle Rock - Crystaroller Room",
|
"Turtle Rock - Big Chest", "Turtle Rock - Crystaroller Room",
|
||||||
"Turtle Rock - Eye Bridge - Bottom Left", "Turtle Rock - Eye Bridge - Bottom Right",
|
"Turtle Rock - Eye Bridge - Bottom Left", "Turtle Rock - Eye Bridge - Bottom Right",
|
||||||
"Turtle Rock - Eye Bridge - Top Left", "Turtle Rock - Eye Bridge - Top Right",
|
"Turtle Rock - Eye Bridge - Top Left", "Turtle Rock - Eye Bridge - Top Right",
|
||||||
|
"Turtle Rock - Pokey 1 Key Drop", "Turtle Rock - Pokey 2 Key Drop",
|
||||||
"Turtle Rock - Boss"},
|
"Turtle Rock - Boss"},
|
||||||
"Ganons Tower": {"Ganons Tower - Bob's Torch", "Ganons Tower - Hope Room - Left",
|
"Ganons Tower": {"Ganons Tower - Bob's Torch", "Ganons Tower - Hope Room - Left",
|
||||||
"Ganons Tower - Hope Room - Right", "Ganons Tower - Tile Room",
|
"Ganons Tower - Hope Room - Right", "Ganons Tower - Tile Room",
|
||||||
@@ -204,10 +220,13 @@ class ALTTPWorld(World):
|
|||||||
"Ganons Tower - Randomizer Room - Bottom Left", "Ganons Tower - Randomizer Room - Bottom Right",
|
"Ganons Tower - Randomizer Room - Bottom Left", "Ganons Tower - Randomizer Room - Bottom Right",
|
||||||
"Ganons Tower - Bob's Chest", "Ganons Tower - Big Chest", "Ganons Tower - Big Key Room - Left",
|
"Ganons Tower - Bob's Chest", "Ganons Tower - Big Chest", "Ganons Tower - Big Key Room - Left",
|
||||||
"Ganons Tower - Big Key Room - Right", "Ganons Tower - Big Key Chest",
|
"Ganons Tower - Big Key Room - Right", "Ganons Tower - Big Key Chest",
|
||||||
"Ganons Tower - Mini Helmasaur Room - Left", "Ganons Tower - Mini Helmasaur Room - Right",
|
"Ganons Tower - Conveyor Cross Pot Key", "Ganons Tower - Conveyor Star Pits Pot Key",
|
||||||
"Ganons Tower - Pre-Moldorm Chest", "Ganons Tower - Validation Chest"},
|
"Ganons Tower - Double Switch Pot Key", "Ganons Tower - Mini Helmasaur Room - Left",
|
||||||
|
"Ganons Tower - Mini Helmasaur Room - Right", "Ganons Tower - Pre-Moldorm Chest",
|
||||||
|
"Ganons Tower - Mini Helmasaur Key Drop", "Ganons Tower - Validation Chest"},
|
||||||
"Ganons Tower Climb": {"Ganons Tower - Mini Helmasaur Room - Left", "Ganons Tower - Mini Helmasaur Room - Right",
|
"Ganons Tower Climb": {"Ganons Tower - Mini Helmasaur Room - Left", "Ganons Tower - Mini Helmasaur Room - Right",
|
||||||
"Ganons Tower - Pre-Moldorm Chest", "Ganons Tower - Validation Chest"},
|
"Ganons Tower - Mini Helmasaur Key Drop", "Ganons Tower - Pre-Moldorm Chest",
|
||||||
|
"Ganons Tower - Validation Chest"},
|
||||||
}
|
}
|
||||||
hint_blacklist = {"Triforce"}
|
hint_blacklist = {"Triforce"}
|
||||||
|
|
||||||
@@ -294,74 +313,62 @@ class ALTTPWorld(World):
|
|||||||
break
|
break
|
||||||
|
|
||||||
def generate_early(self):
|
def generate_early(self):
|
||||||
# write old options
|
|
||||||
import dataclasses
|
|
||||||
is_first = self.player == min(self.multiworld.get_game_players(self.game))
|
|
||||||
|
|
||||||
for field in dataclasses.fields(self.options_dataclass):
|
|
||||||
if is_first:
|
|
||||||
setattr(self.multiworld, field.name, {})
|
|
||||||
getattr(self.multiworld, field.name)[self.player] = getattr(self.options, field.name)
|
|
||||||
# end of old options re-establisher
|
|
||||||
|
|
||||||
player = self.player
|
|
||||||
multiworld = self.multiworld
|
multiworld = self.multiworld
|
||||||
|
|
||||||
self.fix_trock_doors = (multiworld.entrance_shuffle[player] != 'vanilla'
|
self.fix_trock_doors = (self.options.entrance_shuffle != 'vanilla' or self.options.mode == 'inverted')
|
||||||
or multiworld.mode[player] == 'inverted')
|
self.fix_skullwoods_exit = self.options.entrance_shuffle not in ['vanilla', 'simple', 'restricted', 'dungeons_simple']
|
||||||
self.fix_skullwoods_exit = multiworld.entrance_shuffle[player] not in ['vanilla', 'simple', 'restricted',
|
self.fix_palaceofdarkness_exit = self.options.entrance_shuffle not in ['dungeons_simple', 'vanilla', 'simple', 'restricted']
|
||||||
'dungeons_simple']
|
self.fix_trock_exit = self.options.entrance_shuffle not in ['vanilla', 'simple', 'restricted', 'dungeons_simple']
|
||||||
self.fix_palaceofdarkness_exit = multiworld.entrance_shuffle[player] not in ['dungeons_simple', 'vanilla',
|
|
||||||
'simple', 'restricted']
|
|
||||||
self.fix_trock_exit = multiworld.entrance_shuffle[player] not in ['vanilla', 'simple', 'restricted',
|
|
||||||
'dungeons_simple']
|
|
||||||
|
|
||||||
# fairy bottle fills
|
# fairy bottle fills
|
||||||
bottle_options = [
|
bottle_options = [
|
||||||
"Bottle (Red Potion)", "Bottle (Green Potion)", "Bottle (Blue Potion)",
|
"Bottle (Red Potion)", "Bottle (Green Potion)", "Bottle (Blue Potion)",
|
||||||
"Bottle (Bee)", "Bottle (Good Bee)"
|
"Bottle (Bee)", "Bottle (Good Bee)"
|
||||||
]
|
]
|
||||||
if multiworld.item_pool[player] not in ["hard", "expert"]:
|
if self.options.item_pool not in ["hard", "expert"]:
|
||||||
bottle_options.append("Bottle (Fairy)")
|
bottle_options.append("Bottle (Fairy)")
|
||||||
self.waterfall_fairy_bottle_fill = self.random.choice(bottle_options)
|
self.waterfall_fairy_bottle_fill = self.random.choice(bottle_options)
|
||||||
self.pyramid_fairy_bottle_fill = self.random.choice(bottle_options)
|
self.pyramid_fairy_bottle_fill = self.random.choice(bottle_options)
|
||||||
|
|
||||||
if multiworld.mode[player] == 'standard':
|
if self.options.mode == 'standard':
|
||||||
if multiworld.small_key_shuffle[player]:
|
if self.options.small_key_shuffle:
|
||||||
if (multiworld.small_key_shuffle[player] not in
|
if (self.options.small_key_shuffle not in
|
||||||
(small_key_shuffle.option_universal, small_key_shuffle.option_own_dungeons,
|
(small_key_shuffle.option_universal, small_key_shuffle.option_own_dungeons,
|
||||||
small_key_shuffle.option_start_with)):
|
small_key_shuffle.option_start_with)):
|
||||||
self.multiworld.local_early_items[self.player]["Small Key (Hyrule Castle)"] = 1
|
self.multiworld.local_early_items[self.player]["Small Key (Hyrule Castle)"] = 1
|
||||||
self.multiworld.local_items[self.player].value.add("Small Key (Hyrule Castle)")
|
self.options.local_items.value.add("Small Key (Hyrule Castle)")
|
||||||
self.multiworld.non_local_items[self.player].value.discard("Small Key (Hyrule Castle)")
|
self.options.non_local_items.value.discard("Small Key (Hyrule Castle)")
|
||||||
if multiworld.big_key_shuffle[player]:
|
if self.options.big_key_shuffle:
|
||||||
self.multiworld.local_items[self.player].value.add("Big Key (Hyrule Castle)")
|
self.options.local_items.value.add("Big Key (Hyrule Castle)")
|
||||||
self.multiworld.non_local_items[self.player].value.discard("Big Key (Hyrule Castle)")
|
self.options.non_local_items.value.discard("Big Key (Hyrule Castle)")
|
||||||
|
|
||||||
# system for sharing ER layouts
|
# system for sharing ER layouts
|
||||||
self.er_seed = str(multiworld.random.randint(0, 2 ** 64))
|
self.er_seed = str(multiworld.random.randint(0, 2 ** 64))
|
||||||
|
|
||||||
if multiworld.entrance_shuffle[player] != "vanilla" and multiworld.entrance_shuffle_seed[player] != "random":
|
if self.options.entrance_shuffle != "vanilla" and self.options.entrance_shuffle_seed != "random":
|
||||||
shuffle = multiworld.entrance_shuffle[player].current_key
|
shuffle = self.options.entrance_shuffle.current_key
|
||||||
if shuffle == "vanilla":
|
if shuffle == "vanilla":
|
||||||
self.er_seed = "vanilla"
|
self.er_seed = "vanilla"
|
||||||
elif (not multiworld.entrance_shuffle_seed[player].value.isdigit()) or multiworld.is_race:
|
elif (not self.options.entrance_shuffle_seed.value.isdigit()) or multiworld.is_race:
|
||||||
self.er_seed = get_same_seed(multiworld, (
|
self.er_seed = get_same_seed(multiworld, (
|
||||||
shuffle, multiworld.entrance_shuffle_seed[player].value, multiworld.retro_caves[player], multiworld.mode[player],
|
shuffle, self.options.entrance_shuffle_seed.value,
|
||||||
multiworld.glitches_required[player]))
|
self.options.retro_caves,
|
||||||
|
self.options.mode,
|
||||||
|
self.options.glitches_required
|
||||||
|
))
|
||||||
else: # not a race or group seed, use set seed as is.
|
else: # not a race or group seed, use set seed as is.
|
||||||
self.er_seed = int(multiworld.entrance_shuffle_seed[player].value)
|
self.er_seed = int(self.options.entrance_shuffle_seed.value)
|
||||||
elif multiworld.entrance_shuffle[player] == "vanilla":
|
elif self.options.entrance_shuffle == "vanilla":
|
||||||
self.er_seed = "vanilla"
|
self.er_seed = "vanilla"
|
||||||
|
|
||||||
for dungeon_item in ["small_key_shuffle", "big_key_shuffle", "compass_shuffle", "map_shuffle"]:
|
for dungeon_item in ["small_key_shuffle", "big_key_shuffle", "compass_shuffle", "map_shuffle"]:
|
||||||
option = getattr(multiworld, dungeon_item)[player]
|
option = getattr(self.options, dungeon_item)
|
||||||
if option == "own_world":
|
if option == "own_world":
|
||||||
multiworld.local_items[player].value |= self.item_name_groups[option.item_name_group]
|
self.options.local_items.value |= self.item_name_groups[option.item_name_group]
|
||||||
elif option == "different_world":
|
elif option == "different_world":
|
||||||
multiworld.non_local_items[player].value |= self.item_name_groups[option.item_name_group]
|
self.options.non_local_items.value |= self.item_name_groups[option.item_name_group]
|
||||||
if multiworld.mode[player] == "standard":
|
if self.options.mode == "standard":
|
||||||
multiworld.non_local_items[player].value -= {"Small Key (Hyrule Castle)"}
|
self.options.non_local_items.value -= {"Small Key (Hyrule Castle)"}
|
||||||
elif option.in_dungeon:
|
elif option.in_dungeon:
|
||||||
self.dungeon_local_item_names |= self.item_name_groups[option.item_name_group]
|
self.dungeon_local_item_names |= self.item_name_groups[option.item_name_group]
|
||||||
if option == "original_dungeon":
|
if option == "original_dungeon":
|
||||||
@@ -369,15 +376,15 @@ class ALTTPWorld(World):
|
|||||||
else:
|
else:
|
||||||
self.options.local_items.value |= self.dungeon_local_item_names
|
self.options.local_items.value |= self.dungeon_local_item_names
|
||||||
|
|
||||||
self.difficulty_requirements = difficulties[multiworld.item_pool[player].current_key]
|
self.difficulty_requirements = difficulties[self.options.item_pool.current_key]
|
||||||
|
|
||||||
# enforce pre-defined local items.
|
# enforce pre-defined local items.
|
||||||
if multiworld.goal[player] in ["local_triforce_hunt", "local_ganon_triforce_hunt"]:
|
if self.options.goal in ["local_triforce_hunt", "local_ganon_triforce_hunt"]:
|
||||||
multiworld.local_items[player].value.add('Triforce Piece')
|
self.options.local_items.value.add('Triforce Piece')
|
||||||
|
|
||||||
# Not possible to place crystals outside boss prizes yet (might as well make it consistent with pendants too).
|
# Not possible to place crystals outside boss prizes yet (might as well make it consistent with pendants too).
|
||||||
multiworld.non_local_items[player].value -= item_name_groups['Pendants']
|
self.options.non_local_items.value -= item_name_groups['Pendants']
|
||||||
multiworld.non_local_items[player].value -= item_name_groups['Crystals']
|
self.options.non_local_items.value -= item_name_groups['Crystals']
|
||||||
|
|
||||||
create_dungeons = create_dungeons
|
create_dungeons = create_dungeons
|
||||||
|
|
||||||
@@ -385,15 +392,15 @@ class ALTTPWorld(World):
|
|||||||
player = self.player
|
player = self.player
|
||||||
multiworld = self.multiworld
|
multiworld = self.multiworld
|
||||||
|
|
||||||
if multiworld.mode[player] != 'inverted':
|
if self.options.mode != 'inverted':
|
||||||
create_regions(multiworld, player)
|
create_regions(multiworld, player)
|
||||||
else:
|
else:
|
||||||
create_inverted_regions(multiworld, player)
|
create_inverted_regions(multiworld, player)
|
||||||
create_shops(multiworld, player)
|
create_shops(multiworld, player)
|
||||||
self.create_dungeons()
|
self.create_dungeons()
|
||||||
|
|
||||||
if (multiworld.glitches_required[player] not in ["no_glitches", "minor_glitches"] and
|
if (self.options.glitches_required not in ["no_glitches", "minor_glitches"] and
|
||||||
multiworld.entrance_shuffle[player] in [
|
self.options.entrance_shuffle in [
|
||||||
"vanilla", "dungeons_simple", "dungeons_full", "simple", "restricted", "full"]):
|
"vanilla", "dungeons_simple", "dungeons_full", "simple", "restricted", "full"]):
|
||||||
self.fix_fake_world = False
|
self.fix_fake_world = False
|
||||||
|
|
||||||
@@ -401,7 +408,7 @@ class ALTTPWorld(World):
|
|||||||
old_random = multiworld.random
|
old_random = multiworld.random
|
||||||
multiworld.random = random.Random(self.er_seed)
|
multiworld.random = random.Random(self.er_seed)
|
||||||
|
|
||||||
if multiworld.mode[player] != 'inverted':
|
if self.options.mode != 'inverted':
|
||||||
link_entrances(multiworld, player)
|
link_entrances(multiworld, player)
|
||||||
mark_light_world_regions(multiworld, player)
|
mark_light_world_regions(multiworld, player)
|
||||||
else:
|
else:
|
||||||
@@ -486,8 +493,9 @@ class ALTTPWorld(World):
|
|||||||
if state.has('Silver Bow', item.player):
|
if state.has('Silver Bow', item.player):
|
||||||
return
|
return
|
||||||
elif state.has('Bow', item.player) and (self.difficulty_requirements.progressive_bow_limit >= 2
|
elif state.has('Bow', item.player) and (self.difficulty_requirements.progressive_bow_limit >= 2
|
||||||
or self.multiworld.glitches_required[self.player] == 'no_glitches'
|
or self.options.glitches_required == 'no_glitches'
|
||||||
or self.multiworld.swordless[self.player]): # modes where silver bow is always required for ganon
|
or self.options.swordless):
|
||||||
|
# modes where silver bow is always required for ganon
|
||||||
return 'Silver Bow'
|
return 'Silver Bow'
|
||||||
elif self.difficulty_requirements.progressive_bow_limit >= 1:
|
elif self.difficulty_requirements.progressive_bow_limit >= 1:
|
||||||
return 'Bow'
|
return 'Bow'
|
||||||
@@ -530,9 +538,9 @@ class ALTTPWorld(World):
|
|||||||
break
|
break
|
||||||
else:
|
else:
|
||||||
raise FillError('Unable to place dungeon prizes')
|
raise FillError('Unable to place dungeon prizes')
|
||||||
if world.mode[player] == 'standard' and world.small_key_shuffle[player] \
|
if self.options.mode == 'standard' and self.options.small_key_shuffle \
|
||||||
and world.small_key_shuffle[player] != small_key_shuffle.option_universal and \
|
and self.options.small_key_shuffle != small_key_shuffle.option_universal and \
|
||||||
world.small_key_shuffle[player] != small_key_shuffle.option_own_dungeons:
|
self.options.small_key_shuffle != small_key_shuffle.option_own_dungeons:
|
||||||
world.local_early_items[player]["Small Key (Hyrule Castle)"] = 1
|
world.local_early_items[player]["Small Key (Hyrule Castle)"] = 1
|
||||||
|
|
||||||
@classmethod
|
@classmethod
|
||||||
@@ -573,27 +581,27 @@ class ALTTPWorld(World):
|
|||||||
multiworld.spoiler.hashes[player] = get_hash_string(rom.hash)
|
multiworld.spoiler.hashes[player] = get_hash_string(rom.hash)
|
||||||
|
|
||||||
palettes_options = {
|
palettes_options = {
|
||||||
'dungeon': multiworld.uw_palettes[player],
|
'dungeon': self.options.uw_palettes,
|
||||||
'overworld': multiworld.ow_palettes[player],
|
'overworld': self.options.ow_palettes,
|
||||||
'hud': multiworld.hud_palettes[player],
|
'hud': self.options.hud_palettes,
|
||||||
'sword': multiworld.sword_palettes[player],
|
'sword': self.options.sword_palettes,
|
||||||
'shield': multiworld.shield_palettes[player],
|
'shield': self.options.shield_palettes,
|
||||||
# 'link': world.link_palettes[player]
|
# 'link': world.link_palettes[player]
|
||||||
}
|
}
|
||||||
palettes_options = {key: option.current_key for key, option in palettes_options.items()}
|
palettes_options = {key: option.current_key for key, option in palettes_options.items()}
|
||||||
|
|
||||||
apply_rom_settings(rom, multiworld.heartbeep[player].current_key,
|
apply_rom_settings(rom, self.options.heartbeep.current_key,
|
||||||
multiworld.heartcolor[player].current_key,
|
self.options.heartcolor.current_key,
|
||||||
multiworld.quickswap[player],
|
self.options.quickswap,
|
||||||
multiworld.menuspeed[player].current_key,
|
self.options.menuspeed.current_key,
|
||||||
multiworld.music[player],
|
self.options.music,
|
||||||
multiworld.sprite[player],
|
multiworld.sprite[player],
|
||||||
None,
|
None,
|
||||||
palettes_options, multiworld, player, True,
|
palettes_options, multiworld, player, True,
|
||||||
reduceflashing=multiworld.reduceflashing[player] or multiworld.is_race,
|
reduceflashing=self.options.reduceflashing or multiworld.is_race,
|
||||||
triforcehud=multiworld.triforcehud[player].current_key,
|
triforcehud=self.options.triforcehud.current_key,
|
||||||
deathlink=multiworld.death_link[player],
|
deathlink=self.options.death_link,
|
||||||
allowcollect=multiworld.allow_collect[player])
|
allowcollect=self.options.allow_collect)
|
||||||
|
|
||||||
rompath = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.sfc")
|
rompath = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}.sfc")
|
||||||
rom.write_to_file(rompath)
|
rom.write_to_file(rompath)
|
||||||
@@ -610,7 +618,7 @@ class ALTTPWorld(World):
|
|||||||
@classmethod
|
@classmethod
|
||||||
def stage_extend_hint_information(cls, world, hint_data: typing.Dict[int, typing.Dict[int, str]]):
|
def stage_extend_hint_information(cls, world, hint_data: typing.Dict[int, typing.Dict[int, str]]):
|
||||||
er_hint_data = {player: {} for player in world.get_game_players("A Link to the Past") if
|
er_hint_data = {player: {} for player in world.get_game_players("A Link to the Past") if
|
||||||
world.entrance_shuffle[player] != "vanilla" or world.retro_caves[player]}
|
world.worlds[player].options.entrance_shuffle != "vanilla" or world.worlds[player].options.retro_caves}
|
||||||
|
|
||||||
for region in world.regions:
|
for region in world.regions:
|
||||||
if region.player in er_hint_data and region.locations:
|
if region.player in er_hint_data and region.locations:
|
||||||
@@ -726,7 +734,7 @@ class ALTTPWorld(World):
|
|||||||
f" {self.pyramid_fairy_bottle_fill}")
|
f" {self.pyramid_fairy_bottle_fill}")
|
||||||
spoiler_handle.write(f"\nWaterfall Fairy ({player_name}):"
|
spoiler_handle.write(f"\nWaterfall Fairy ({player_name}):"
|
||||||
f" {self.waterfall_fairy_bottle_fill}")
|
f" {self.waterfall_fairy_bottle_fill}")
|
||||||
if self.multiworld.boss_shuffle[self.player] != "none":
|
if self.options.boss_shuffle != "none":
|
||||||
def create_boss_map() -> typing.Dict:
|
def create_boss_map() -> typing.Dict:
|
||||||
boss_map = {
|
boss_map = {
|
||||||
"Eastern Palace": self.dungeons["Eastern Palace"].boss.name,
|
"Eastern Palace": self.dungeons["Eastern Palace"].boss.name,
|
||||||
@@ -743,7 +751,7 @@ class ALTTPWorld(World):
|
|||||||
"Ganons Tower": "Agahnim 2",
|
"Ganons Tower": "Agahnim 2",
|
||||||
"Ganon": "Ganon"
|
"Ganon": "Ganon"
|
||||||
}
|
}
|
||||||
if self.multiworld.mode[self.player] != 'inverted':
|
if self.options.mode != 'inverted':
|
||||||
boss_map.update({
|
boss_map.update({
|
||||||
"Ganons Tower Basement":
|
"Ganons Tower Basement":
|
||||||
self.dungeons["Ganons Tower"].bosses["bottom"].name,
|
self.dungeons["Ganons Tower"].bosses["bottom"].name,
|
||||||
@@ -828,7 +836,7 @@ class ALTTPWorld(World):
|
|||||||
"triforce_pieces_available", "triforce_pieces_extra",
|
"triforce_pieces_available", "triforce_pieces_extra",
|
||||||
]
|
]
|
||||||
|
|
||||||
slot_data = {option_name: getattr(self.multiworld, option_name)[self.player].value for option_name in slot_options}
|
slot_data = {option_name: getattr(self.options, option_name).value for option_name in slot_options}
|
||||||
|
|
||||||
slot_data.update({
|
slot_data.update({
|
||||||
'mm_medalion': self.required_medallions[0],
|
'mm_medalion': self.required_medallions[0],
|
||||||
@@ -849,8 +857,8 @@ def get_same_seed(world, seed_def: tuple) -> str:
|
|||||||
|
|
||||||
class ALttPLogic(LogicMixin):
|
class ALttPLogic(LogicMixin):
|
||||||
def _lttp_has_key(self, item, player, count: int = 1):
|
def _lttp_has_key(self, item, player, count: int = 1):
|
||||||
if self.multiworld.glitches_required[player] == 'no_logic':
|
if self.multiworld.worlds[player].options.glitches_required == 'no_logic':
|
||||||
return True
|
return True
|
||||||
if self.multiworld.small_key_shuffle[player] == small_key_shuffle.option_universal:
|
if self.multiworld.worlds[player].options.small_key_shuffle == small_key_shuffle.option_universal:
|
||||||
return can_buy_unlimited(self, 'Small Key (Universal)', player)
|
return can_buy_unlimited(self, 'Small Key (Universal)', player)
|
||||||
return self.prog_items[player][item] >= count
|
return self.prog_items[player][item] >= count
|
||||||
|
|||||||
@@ -14,8 +14,8 @@ class TestDungeon(LTTPTestBase):
|
|||||||
self.starting_regions = [] # Where to start exploring
|
self.starting_regions = [] # Where to start exploring
|
||||||
self.remove_exits = [] # Block dungeon exits
|
self.remove_exits = [] # Block dungeon exits
|
||||||
self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
|
self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
|
||||||
self.multiworld.bombless_start[1].value = True
|
self.multiworld.worlds[1].options.bombless_start.value = True
|
||||||
self.multiworld.shuffle_capacity_upgrades[1].value = 2
|
self.multiworld.worlds[1].options.shuffle_capacity_upgrades.value = 2
|
||||||
create_regions(self.multiworld, 1)
|
create_regions(self.multiworld, 1)
|
||||||
self.multiworld.worlds[1].create_dungeons()
|
self.multiworld.worlds[1].create_dungeons()
|
||||||
create_shops(self.multiworld, 1)
|
create_shops(self.multiworld, 1)
|
||||||
|
|||||||
@@ -24,7 +24,7 @@ class TestSwampPalace(TestDungeon):
|
|||||||
["Swamp Palace - Big Key Chest", False, [], ['Open Floodgate']],
|
["Swamp Palace - Big Key Chest", False, [], ['Open Floodgate']],
|
||||||
["Swamp Palace - Big Key Chest", False, [], ['Hammer']],
|
["Swamp Palace - Big Key Chest", False, [], ['Hammer']],
|
||||||
["Swamp Palace - Big Key Chest", False, [], ['Small Key (Swamp Palace)']],
|
["Swamp Palace - Big Key Chest", False, [], ['Small Key (Swamp Palace)']],
|
||||||
["Swamp Palace - Big Key Chest", True, ['Open Floodgate', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Flippers', 'Hammer']],
|
["Swamp Palace - Big Key Chest", True, ['Open Floodgate', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Flippers', 'Hammer']],
|
||||||
|
|
||||||
["Swamp Palace - Map Chest", False, []],
|
["Swamp Palace - Map Chest", False, []],
|
||||||
["Swamp Palace - Map Chest", False, [], ['Flippers']],
|
["Swamp Palace - Map Chest", False, [], ['Flippers']],
|
||||||
@@ -38,7 +38,7 @@ class TestSwampPalace(TestDungeon):
|
|||||||
["Swamp Palace - West Chest", False, [], ['Open Floodgate']],
|
["Swamp Palace - West Chest", False, [], ['Open Floodgate']],
|
||||||
["Swamp Palace - West Chest", False, [], ['Hammer']],
|
["Swamp Palace - West Chest", False, [], ['Hammer']],
|
||||||
["Swamp Palace - West Chest", False, [], ['Small Key (Swamp Palace)']],
|
["Swamp Palace - West Chest", False, [], ['Small Key (Swamp Palace)']],
|
||||||
["Swamp Palace - West Chest", True, ['Open Floodgate', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Flippers', 'Hammer']],
|
["Swamp Palace - West Chest", True, ['Open Floodgate', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Small Key (Swamp Palace)', 'Flippers', 'Hammer']],
|
||||||
|
|
||||||
["Swamp Palace - Compass Chest", False, []],
|
["Swamp Palace - Compass Chest", False, []],
|
||||||
["Swamp Palace - Compass Chest", False, [], ['Flippers']],
|
["Swamp Palace - Compass Chest", False, [], ['Flippers']],
|
||||||
|
|||||||
@@ -14,9 +14,9 @@ class TestInverted(TestBase, LTTPTestBase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.world_setup()
|
self.world_setup()
|
||||||
self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
|
self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
|
||||||
self.multiworld.mode[1].value = 2
|
self.multiworld.worlds[1].options.mode.value = 2
|
||||||
self.multiworld.bombless_start[1].value = True
|
self.multiworld.worlds[1].options.bombless_start.value = True
|
||||||
self.multiworld.shuffle_capacity_upgrades[1].value = 2
|
self.multiworld.worlds[1].options.shuffle_capacity_upgrades.value = 2
|
||||||
create_inverted_regions(self.multiworld, 1)
|
create_inverted_regions(self.multiworld, 1)
|
||||||
self.world.create_dungeons()
|
self.world.create_dungeons()
|
||||||
create_shops(self.multiworld, 1)
|
create_shops(self.multiworld, 1)
|
||||||
|
|||||||
@@ -12,7 +12,7 @@ class TestInvertedBombRules(LTTPTestBase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.world_setup()
|
self.world_setup()
|
||||||
self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
|
self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
|
||||||
self.multiworld.mode[1].value = 2
|
self.multiworld.worlds[1].options.mode.value = 2
|
||||||
create_inverted_regions(self.multiworld, 1)
|
create_inverted_regions(self.multiworld, 1)
|
||||||
self.multiworld.worlds[1].create_dungeons()
|
self.multiworld.worlds[1].create_dungeons()
|
||||||
|
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ from worlds.alttp.test import LTTPTestBase
|
|||||||
class TestInvertedMinor(TestBase, LTTPTestBase):
|
class TestInvertedMinor(TestBase, LTTPTestBase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.world_setup()
|
self.world_setup()
|
||||||
self.multiworld.mode[1].value = 2
|
self.multiworld.worlds[1].options.mode.value = 2
|
||||||
self.multiworld.glitches_required[1] = GlitchesRequired.from_any("minor_glitches")
|
self.multiworld.worlds[1].options.glitches_required = GlitchesRequired.from_any("minor_glitches")
|
||||||
self.multiworld.bombless_start[1].value = True
|
self.multiworld.worlds[1].options.bombless_start.value = True
|
||||||
self.multiworld.shuffle_capacity_upgrades[1].value = 2
|
self.multiworld.worlds[1].options.shuffle_capacity_upgrades.value = 2
|
||||||
self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
|
self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
|
||||||
create_inverted_regions(self.multiworld, 1)
|
create_inverted_regions(self.multiworld, 1)
|
||||||
self.world.create_dungeons()
|
self.world.create_dungeons()
|
||||||
|
|||||||
@@ -14,10 +14,10 @@ from worlds.alttp.test import LTTPTestBase
|
|||||||
class TestInvertedOWG(TestBase, LTTPTestBase):
|
class TestInvertedOWG(TestBase, LTTPTestBase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.world_setup()
|
self.world_setup()
|
||||||
self.multiworld.glitches_required[1] = GlitchesRequired.from_any("overworld_glitches")
|
self.multiworld.worlds[1].options.glitches_required = GlitchesRequired.from_any("overworld_glitches")
|
||||||
self.multiworld.mode[1].value = 2
|
self.multiworld.worlds[1].options.mode.value = 2
|
||||||
self.multiworld.bombless_start[1].value = True
|
self.multiworld.worlds[1].options.bombless_start.value = True
|
||||||
self.multiworld.shuffle_capacity_upgrades[1].value = 2
|
self.multiworld.worlds[1].options.shuffle_capacity_upgrades.value = 2
|
||||||
self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
|
self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
|
||||||
create_inverted_regions(self.multiworld, 1)
|
create_inverted_regions(self.multiworld, 1)
|
||||||
self.world.create_dungeons()
|
self.world.create_dungeons()
|
||||||
|
|||||||
@@ -11,9 +11,9 @@ from worlds.alttp.test import LTTPTestBase
|
|||||||
class TestMinor(TestBase, LTTPTestBase):
|
class TestMinor(TestBase, LTTPTestBase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.world_setup()
|
self.world_setup()
|
||||||
self.multiworld.glitches_required[1] = GlitchesRequired.from_any("minor_glitches")
|
self.multiworld.worlds[1].options.glitches_required = GlitchesRequired.from_any("minor_glitches")
|
||||||
self.multiworld.bombless_start[1].value = True
|
self.multiworld.worlds[1].options.bombless_start.value = True
|
||||||
self.multiworld.shuffle_capacity_upgrades[1].value = 2
|
self.multiworld.worlds[1].options.shuffle_capacity_upgrades.value = 2
|
||||||
self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
|
self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
|
||||||
self.world.er_seed = 0
|
self.world.er_seed = 0
|
||||||
self.world.create_regions()
|
self.world.create_regions()
|
||||||
|
|||||||
@@ -23,7 +23,7 @@ class GoalPyramidTest(PyramidTestBase):
|
|||||||
}
|
}
|
||||||
|
|
||||||
def testCrystalsGoalAccess(self):
|
def testCrystalsGoalAccess(self):
|
||||||
self.multiworld.goal[1].value = 1 # crystals
|
self.multiworld.worlds[1].options.goal.value = 1 # crystals
|
||||||
self.assertFalse(self.can_reach_entrance("Pyramid Hole"))
|
self.assertFalse(self.can_reach_entrance("Pyramid Hole"))
|
||||||
self.collect_by_name(["Hammer", "Progressive Glove", "Moon Pearl"])
|
self.collect_by_name(["Hammer", "Progressive Glove", "Moon Pearl"])
|
||||||
self.assertTrue(self.can_reach_entrance("Pyramid Hole"))
|
self.assertTrue(self.can_reach_entrance("Pyramid Hole"))
|
||||||
|
|||||||
@@ -12,9 +12,9 @@ class TestVanillaOWG(TestBase, LTTPTestBase):
|
|||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.world_setup()
|
self.world_setup()
|
||||||
self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
|
self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
|
||||||
self.multiworld.glitches_required[1] = GlitchesRequired.from_any("overworld_glitches")
|
self.multiworld.worlds[1].options.glitches_required = GlitchesRequired.from_any("overworld_glitches")
|
||||||
self.multiworld.bombless_start[1].value = True
|
self.multiworld.worlds[1].options.bombless_start.value = True
|
||||||
self.multiworld.shuffle_capacity_upgrades[1].value = 2
|
self.multiworld.worlds[1].options.shuffle_capacity_upgrades.value = 2
|
||||||
self.multiworld.worlds[1].er_seed = 0
|
self.multiworld.worlds[1].er_seed = 0
|
||||||
self.multiworld.worlds[1].create_regions()
|
self.multiworld.worlds[1].create_regions()
|
||||||
self.multiworld.worlds[1].create_items()
|
self.multiworld.worlds[1].create_items()
|
||||||
|
|||||||
@@ -10,10 +10,10 @@ from worlds.alttp.test import LTTPTestBase
|
|||||||
class TestVanilla(TestBase, LTTPTestBase):
|
class TestVanilla(TestBase, LTTPTestBase):
|
||||||
def setUp(self):
|
def setUp(self):
|
||||||
self.world_setup()
|
self.world_setup()
|
||||||
self.multiworld.glitches_required[1] = GlitchesRequired.from_any("no_glitches")
|
self.multiworld.worlds[1].options.glitches_required = GlitchesRequired.from_any("no_glitches")
|
||||||
self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
|
self.multiworld.worlds[1].difficulty_requirements = difficulties['normal']
|
||||||
self.multiworld.bombless_start[1].value = True
|
self.multiworld.worlds[1].options.bombless_start.value = True
|
||||||
self.multiworld.shuffle_capacity_upgrades[1].value = 2
|
self.multiworld.worlds[1].options.shuffle_capacity_upgrades.value = 2
|
||||||
self.multiworld.worlds[1].er_seed = 0
|
self.multiworld.worlds[1].er_seed = 0
|
||||||
self.multiworld.worlds[1].create_regions()
|
self.multiworld.worlds[1].create_regions()
|
||||||
self.multiworld.worlds[1].create_items()
|
self.multiworld.worlds[1].create_items()
|
||||||
|
|||||||
@@ -16,8 +16,11 @@ Cyberfunk root folder. *Do not use any pre-release versions of BepInEx 6.*
|
|||||||
|
|
||||||
2. Start Bomb Rush Cyberfunk once so that BepInEx can create its required configuration files.
|
2. Start Bomb Rush Cyberfunk once so that BepInEx can create its required configuration files.
|
||||||
|
|
||||||
3. Download the zip archive from the [releases](https://github.com/TRPG0/BRC-Archipelago/releases) page, and extract its
|
3. Download `ModLocalizer.dll` from its [releases](https://github.com/TRPG0/BRC-ModLocalizer/releases) page, and put it
|
||||||
contents into `BepInEx\plugins`.
|
in `BepInEx\plugins`.
|
||||||
|
|
||||||
|
4. Download the zip archive for the Archipelago plugin from its [releases](https://github.com/TRPG0/BRC-Archipelago/releases)
|
||||||
|
page, and extract the contents into `BepInEx\plugins`.
|
||||||
|
|
||||||
After installing Archipelago, there are some additional mods that can also be installed for a better experience:
|
After installing Archipelago, there are some additional mods that can also be installed for a better experience:
|
||||||
|
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ config file.
|
|||||||
|
|
||||||
## What is considered a location check in ChecksFinder?
|
## What is considered a location check in ChecksFinder?
|
||||||
|
|
||||||
Location checks in are completed when the player finds a spot on a board that has the archipelago logo. The bottom of
|
Location checks get cleared when you open all non-bomb cells in a board. The bottom
|
||||||
the screen has a number next to the archipelago logo, that number is how many you can find so far. You can only get as
|
of the screen has a number next to the Archipelago logo that displays how many location checks are left to be sent with
|
||||||
many checks as you have gained items, plus five to start with being available.
|
your current inventory. You can only get as many checks as you have gained items plus five checks to start with.
|
||||||
|
|
||||||
## When the player receives an item, what happens?
|
## When the player receives an item, what happens?
|
||||||
|
|
||||||
|
|||||||
@@ -10,12 +10,20 @@ from worlds._bizhawk.client import BizHawkClient
|
|||||||
if TYPE_CHECKING:
|
if TYPE_CHECKING:
|
||||||
from worlds._bizhawk.context import BizHawkClientContext
|
from worlds._bizhawk.context import BizHawkClientContext
|
||||||
|
|
||||||
|
DEATHLINK_AREA_NUMBERS = [0, 1, 1, 2, 2, 2, 2, 3, 4, 5, 5, 5, 5, 5, 5, 5,
|
||||||
|
7, 9, 8, 6, 12, 12, 13, 11, 12, 5, 2, 10, 13, 13]
|
||||||
|
|
||||||
|
DEATHLINK_AREA_NAMES = ["Forest of Silence", "Castle Wall", "Villa", "Tunnel", "Underground Waterway", "Castle Center",
|
||||||
|
"Duel Tower", "Tower of Execution", "Tower of Science", "Tower of Sorcery", "Room of Clocks",
|
||||||
|
"Clock Tower", "Castle Keep", "Level: You Cheated"]
|
||||||
|
|
||||||
|
|
||||||
class Castlevania64Client(BizHawkClient):
|
class Castlevania64Client(BizHawkClient):
|
||||||
game = "Castlevania 64"
|
game = "Castlevania 64"
|
||||||
system = "N64"
|
system = "N64"
|
||||||
patch_suffix = ".apcv64"
|
patch_suffix = ".apcv64"
|
||||||
self_induced_death = False
|
self_induced_death = False
|
||||||
|
time_of_sent_death = None
|
||||||
received_deathlinks = 0
|
received_deathlinks = 0
|
||||||
death_causes = []
|
death_causes = []
|
||||||
currently_shopping = False
|
currently_shopping = False
|
||||||
@@ -62,15 +70,19 @@ class Castlevania64Client(BizHawkClient):
|
|||||||
return
|
return
|
||||||
if "tags" not in args:
|
if "tags" not in args:
|
||||||
return
|
return
|
||||||
if "DeathLink" in args["tags"] and args["data"]["source"] != ctx.slot_info[ctx.slot].name:
|
if "DeathLink" in args["tags"] and args["data"]["time"] != self.time_of_sent_death:
|
||||||
self.received_deathlinks += 1
|
self.received_deathlinks += 1
|
||||||
if "cause" in args["data"]:
|
if "cause" in args["data"]:
|
||||||
cause = args["data"]["cause"]
|
cause = args["data"]["cause"]
|
||||||
|
# If the other game sent a death with a blank string for the cause, use the default death message.
|
||||||
|
if cause == "":
|
||||||
|
cause = f"{args['data']['source']} killed you without a word!"
|
||||||
# Truncate the death cause message at 120 characters.
|
# Truncate the death cause message at 120 characters.
|
||||||
if len(cause) > 120:
|
if len(cause) > 120:
|
||||||
cause = cause[0:120]
|
cause = cause[0:120]
|
||||||
else:
|
else:
|
||||||
cause = f"{args['data']['source']} killed you!"
|
# If the other game sent a death with no cause at all, use the default death message.
|
||||||
|
cause = f"{args['data']['source']} killed you without a word!"
|
||||||
self.death_causes.append(cause)
|
self.death_causes.append(cause)
|
||||||
|
|
||||||
async def game_watcher(self, ctx: "BizHawkClientContext") -> None:
|
async def game_watcher(self, ctx: "BizHawkClientContext") -> None:
|
||||||
@@ -115,11 +127,30 @@ class Castlevania64Client(BizHawkClient):
|
|||||||
if "DeathLink" in ctx.tags and save_struct[0xA4] & 0x80 and not self.self_induced_death and not \
|
if "DeathLink" in ctx.tags and save_struct[0xA4] & 0x80 and not self.self_induced_death and not \
|
||||||
deathlink_induced_death:
|
deathlink_induced_death:
|
||||||
self.self_induced_death = True
|
self.self_induced_death = True
|
||||||
if save_struct[0xA4] & 0x08:
|
|
||||||
# Special death message for dying while having the Vamp status.
|
# If the player died at the Castle Keep exterior map on one of the Room of Clocks boss towers
|
||||||
await ctx.send_death(f"{ctx.player_names[ctx.slot]} became a vampire and drank your blood!")
|
# (determinable by checking the entrance value as well as the map value), consider Room of Clocks the
|
||||||
|
# actual area of death.
|
||||||
|
if save_struct[0xAD] == 0x14 and save_struct[0xAF] in [0, 1]:
|
||||||
|
area_of_death = DEATHLINK_AREA_NAMES[10]
|
||||||
|
# Otherwise, determine what area the player perished in from the current map ID.
|
||||||
else:
|
else:
|
||||||
await ctx.send_death(f"{ctx.player_names[ctx.slot]} perished. Dracula has won!")
|
area_of_death = DEATHLINK_AREA_NAMES[DEATHLINK_AREA_NUMBERS[save_struct[0xAD]]]
|
||||||
|
|
||||||
|
# If we had the Vamp status while dying, use a special message.
|
||||||
|
if save_struct[0xA4] & 0x08:
|
||||||
|
death_message = (f"{ctx.player_names[ctx.slot]} became a vampire at {area_of_death} and drank your "
|
||||||
|
f"blood!")
|
||||||
|
# Otherwise, use the generic one.
|
||||||
|
else:
|
||||||
|
death_message = f"{ctx.player_names[ctx.slot]} perished in {area_of_death}. Dracula has won!"
|
||||||
|
|
||||||
|
# Send the death.
|
||||||
|
await ctx.send_death(death_message)
|
||||||
|
|
||||||
|
# Record the time in which the death was sent so when we receive the packet we can tell it wasn't our
|
||||||
|
# own death. ctx.on_deathlink overwrites it later, so it MUST be grabbed now.
|
||||||
|
self.time_of_sent_death = ctx.last_death_link
|
||||||
|
|
||||||
# Write any DeathLinks received along with the corresponding death cause starting with the oldest.
|
# Write any DeathLinks received along with the corresponding death cause starting with the oldest.
|
||||||
# To minimize Bizhawk Write jank, the DeathLink write will be prioritized over the item received one.
|
# To minimize Bizhawk Write jank, the DeathLink write will be prioritized over the item received one.
|
||||||
@@ -208,6 +239,7 @@ class Castlevania64Client(BizHawkClient):
|
|||||||
|
|
||||||
# Send game clear if we're in either any ending cutscene or the credits state.
|
# Send game clear if we're in either any ending cutscene or the credits state.
|
||||||
if not ctx.finished_game and (0x26 <= int(cutscene_value) <= 0x2E or game_state == 0x0000000B):
|
if not ctx.finished_game and (0x26 <= int(cutscene_value) <= 0x2E or game_state == 0x0000000B):
|
||||||
|
ctx.finished_game = True
|
||||||
await ctx.send_msgs([{
|
await ctx.send_msgs([{
|
||||||
"cmd": "StatusUpdate",
|
"cmd": "StatusUpdate",
|
||||||
"status": ClientStatus.CLIENT_GOAL
|
"status": ClientStatus.CLIENT_GOAL
|
||||||
|
|||||||
@@ -9,7 +9,6 @@ import random
|
|||||||
import re
|
import re
|
||||||
import string
|
import string
|
||||||
import subprocess
|
import subprocess
|
||||||
|
|
||||||
import sys
|
import sys
|
||||||
import time
|
import time
|
||||||
import typing
|
import typing
|
||||||
@@ -17,15 +16,16 @@ from queue import Queue
|
|||||||
|
|
||||||
import factorio_rcon
|
import factorio_rcon
|
||||||
|
|
||||||
import Utils
|
|
||||||
from CommonClient import ClientCommandProcessor, CommonContext, logger, server_loop, gui_enabled, get_base_parser
|
from CommonClient import ClientCommandProcessor, CommonContext, logger, server_loop, gui_enabled, get_base_parser
|
||||||
from MultiServer import mark_raw
|
from MultiServer import mark_raw
|
||||||
from NetUtils import ClientStatus, NetworkItem, JSONtoTextParser, JSONMessagePart
|
from NetUtils import ClientStatus, NetworkItem, JSONtoTextParser, JSONMessagePart
|
||||||
from Utils import async_start, get_file_safe_name
|
from Utils import async_start, get_file_safe_name, is_windows, Version, format_SI_prefix, get_text_between
|
||||||
|
from .settings import FactorioSettings
|
||||||
|
from settings import get_settings
|
||||||
|
|
||||||
|
|
||||||
def check_stdin() -> None:
|
def check_stdin() -> None:
|
||||||
if Utils.is_windows and sys.stdin:
|
if is_windows and sys.stdin:
|
||||||
print("WARNING: Console input is not routed reliably on Windows, use the GUI instead.")
|
print("WARNING: Console input is not routed reliably on Windows, use the GUI instead.")
|
||||||
|
|
||||||
|
|
||||||
@@ -67,7 +67,7 @@ class FactorioContext(CommonContext):
|
|||||||
items_handling = 0b111 # full remote
|
items_handling = 0b111 # full remote
|
||||||
|
|
||||||
# updated by spinup server
|
# updated by spinup server
|
||||||
mod_version: Utils.Version = Utils.Version(0, 0, 0)
|
mod_version: Version = Version(0, 0, 0)
|
||||||
|
|
||||||
def __init__(self, server_address, password, filter_item_sends: bool, bridge_chat_out: bool):
|
def __init__(self, server_address, password, filter_item_sends: bool, bridge_chat_out: bool):
|
||||||
super(FactorioContext, self).__init__(server_address, password)
|
super(FactorioContext, self).__init__(server_address, password)
|
||||||
@@ -133,7 +133,7 @@ class FactorioContext(CommonContext):
|
|||||||
elif self.current_energy_link_value is None:
|
elif self.current_energy_link_value is None:
|
||||||
return "Standby"
|
return "Standby"
|
||||||
else:
|
else:
|
||||||
return f"{Utils.format_SI_prefix(self.current_energy_link_value)}J"
|
return f"{format_SI_prefix(self.current_energy_link_value)}J"
|
||||||
|
|
||||||
def on_deathlink(self, data: dict):
|
def on_deathlink(self, data: dict):
|
||||||
if self.rcon_client:
|
if self.rcon_client:
|
||||||
@@ -155,10 +155,10 @@ class FactorioContext(CommonContext):
|
|||||||
if self.energy_link_increment and args.get("last_deplete", -1) == self.last_deplete:
|
if self.energy_link_increment and args.get("last_deplete", -1) == self.last_deplete:
|
||||||
# it's our deplete request
|
# it's our deplete request
|
||||||
gained = int(args["original_value"] - args["value"])
|
gained = int(args["original_value"] - args["value"])
|
||||||
gained_text = Utils.format_SI_prefix(gained) + "J"
|
gained_text = format_SI_prefix(gained) + "J"
|
||||||
if gained:
|
if gained:
|
||||||
logger.debug(f"EnergyLink: Received {gained_text}. "
|
logger.debug(f"EnergyLink: Received {gained_text}. "
|
||||||
f"{Utils.format_SI_prefix(args['value'])}J remaining.")
|
f"{format_SI_prefix(args['value'])}J remaining.")
|
||||||
self.rcon_client.send_command(f"/ap-energylink {gained}")
|
self.rcon_client.send_command(f"/ap-energylink {gained}")
|
||||||
|
|
||||||
def on_user_say(self, text: str) -> typing.Optional[str]:
|
def on_user_say(self, text: str) -> typing.Optional[str]:
|
||||||
@@ -278,7 +278,7 @@ async def game_watcher(ctx: FactorioContext):
|
|||||||
}]))
|
}]))
|
||||||
ctx.rcon_client.send_command(
|
ctx.rcon_client.send_command(
|
||||||
f"/ap-energylink -{value}")
|
f"/ap-energylink -{value}")
|
||||||
logger.debug(f"EnergyLink: Sent {Utils.format_SI_prefix(value)}J")
|
logger.debug(f"EnergyLink: Sent {format_SI_prefix(value)}J")
|
||||||
|
|
||||||
await asyncio.sleep(0.1)
|
await asyncio.sleep(0.1)
|
||||||
|
|
||||||
@@ -439,9 +439,9 @@ async def factorio_spinup_server(ctx: FactorioContext) -> bool:
|
|||||||
factorio_server_logger.info(msg)
|
factorio_server_logger.info(msg)
|
||||||
if "Loading mod AP-" in msg and msg.endswith("(data.lua)"):
|
if "Loading mod AP-" in msg and msg.endswith("(data.lua)"):
|
||||||
parts = msg.split()
|
parts = msg.split()
|
||||||
ctx.mod_version = Utils.Version(*(int(number) for number in parts[-2].split(".")))
|
ctx.mod_version = Version(*(int(number) for number in parts[-2].split(".")))
|
||||||
elif "Write data path: " in msg:
|
elif "Write data path: " in msg:
|
||||||
ctx.write_data_path = Utils.get_text_between(msg, "Write data path: ", " [")
|
ctx.write_data_path = get_text_between(msg, "Write data path: ", " [")
|
||||||
if "AppData" in ctx.write_data_path:
|
if "AppData" in ctx.write_data_path:
|
||||||
logger.warning("It appears your mods are loaded from Appdata, "
|
logger.warning("It appears your mods are loaded from Appdata, "
|
||||||
"this can lead to problems with multiple Factorio instances. "
|
"this can lead to problems with multiple Factorio instances. "
|
||||||
@@ -521,10 +521,16 @@ rcon_port = args.rcon_port
|
|||||||
rcon_password = args.rcon_password if args.rcon_password else ''.join(
|
rcon_password = args.rcon_password if args.rcon_password else ''.join(
|
||||||
random.choice(string.ascii_letters) for x in range(32))
|
random.choice(string.ascii_letters) for x in range(32))
|
||||||
factorio_server_logger = logging.getLogger("FactorioServer")
|
factorio_server_logger = logging.getLogger("FactorioServer")
|
||||||
options = Utils.get_settings()
|
settings: FactorioSettings = get_settings().factorio_options
|
||||||
executable = options["factorio_options"]["executable"]
|
if os.path.samefile(settings.executable, sys.executable):
|
||||||
|
selected_executable = settings.executable
|
||||||
|
settings.executable = FactorioSettings.executable # reset to default
|
||||||
|
raise Exception(f"FactorioClient was set to run itself {selected_executable}, aborting process bomb.")
|
||||||
|
|
||||||
|
executable = settings.executable
|
||||||
|
|
||||||
server_settings = args.server_settings if args.server_settings \
|
server_settings = args.server_settings if args.server_settings \
|
||||||
else options["factorio_options"].get("server_settings", None)
|
else getattr(settings, "server_settings", None)
|
||||||
server_args = ("--rcon-port", rcon_port, "--rcon-password", rcon_password)
|
server_args = ("--rcon-port", rcon_port, "--rcon-password", rcon_password)
|
||||||
|
|
||||||
|
|
||||||
@@ -535,12 +541,8 @@ def launch():
|
|||||||
|
|
||||||
if server_settings:
|
if server_settings:
|
||||||
server_settings = os.path.abspath(server_settings)
|
server_settings = os.path.abspath(server_settings)
|
||||||
if not isinstance(options["factorio_options"]["filter_item_sends"], bool):
|
initial_filter_item_sends = bool(settings.filter_item_sends)
|
||||||
logging.warning(f"Warning: Option filter_item_sends should be a bool.")
|
initial_bridge_chat_out = bool(settings.bridge_chat_out)
|
||||||
initial_filter_item_sends = bool(options["factorio_options"]["filter_item_sends"])
|
|
||||||
if not isinstance(options["factorio_options"]["bridge_chat_out"], bool):
|
|
||||||
logging.warning(f"Warning: Option bridge_chat_out should be a bool.")
|
|
||||||
initial_bridge_chat_out = bool(options["factorio_options"]["bridge_chat_out"])
|
|
||||||
|
|
||||||
if not os.path.exists(os.path.dirname(executable)):
|
if not os.path.exists(os.path.dirname(executable)):
|
||||||
raise FileNotFoundError(f"Path {os.path.dirname(executable)} does not exist or could not be accessed.")
|
raise FileNotFoundError(f"Path {os.path.dirname(executable)} does not exist or could not be accessed.")
|
||||||
|
|||||||
@@ -5,7 +5,6 @@ import logging
|
|||||||
import typing
|
import typing
|
||||||
|
|
||||||
import Utils
|
import Utils
|
||||||
import settings
|
|
||||||
from BaseClasses import Region, Location, Item, Tutorial, ItemClassification
|
from BaseClasses import Region, Location, Item, Tutorial, ItemClassification
|
||||||
from worlds.AutoWorld import World, WebWorld
|
from worlds.AutoWorld import World, WebWorld
|
||||||
from worlds.LauncherComponents import Component, components, Type, launch as launch_component
|
from worlds.LauncherComponents import Component, components, Type, launch as launch_component
|
||||||
@@ -20,6 +19,7 @@ from .Technologies import base_tech_table, recipe_sources, base_technology_table
|
|||||||
progressive_technology_table, common_tech_table, tech_to_progressive_lookup, progressive_tech_table, \
|
progressive_technology_table, common_tech_table, tech_to_progressive_lookup, progressive_tech_table, \
|
||||||
get_science_pack_pools, Recipe, recipes, technology_table, tech_table, factorio_base_id, useless_technologies, \
|
get_science_pack_pools, Recipe, recipes, technology_table, tech_table, factorio_base_id, useless_technologies, \
|
||||||
fluids, stacking_items, valid_ingredients, progressive_rows
|
fluids, stacking_items, valid_ingredients, progressive_rows
|
||||||
|
from .settings import FactorioSettings
|
||||||
|
|
||||||
|
|
||||||
def launch_client():
|
def launch_client():
|
||||||
@@ -27,30 +27,7 @@ def launch_client():
|
|||||||
launch_component(launch, name="FactorioClient")
|
launch_component(launch, name="FactorioClient")
|
||||||
|
|
||||||
|
|
||||||
components.append(Component("Factorio Client", "FactorioClient", func=launch_client, component_type=Type.CLIENT))
|
components.append(Component("Factorio Client", func=launch_client, component_type=Type.CLIENT))
|
||||||
|
|
||||||
|
|
||||||
class FactorioSettings(settings.Group):
|
|
||||||
class Executable(settings.UserFilePath):
|
|
||||||
is_exe = True
|
|
||||||
|
|
||||||
class ServerSettings(settings.OptionalUserFilePath):
|
|
||||||
"""
|
|
||||||
by default, no settings are loaded if this file does not exist. \
|
|
||||||
If this file does exist, then it will be used.
|
|
||||||
server_settings: "factorio\\\\data\\\\server-settings.json"
|
|
||||||
"""
|
|
||||||
|
|
||||||
class FilterItemSends(settings.Bool):
|
|
||||||
"""Whether to filter item send messages displayed in-game to only those that involve you."""
|
|
||||||
|
|
||||||
class BridgeChatOut(settings.Bool):
|
|
||||||
"""Whether to send chat messages from players on the Factorio server to Archipelago."""
|
|
||||||
|
|
||||||
executable: Executable = Executable("factorio/bin/x64/factorio")
|
|
||||||
server_settings: typing.Optional[FactorioSettings.ServerSettings] = None
|
|
||||||
filter_item_sends: typing.Union[FilterItemSends, bool] = False
|
|
||||||
bridge_chat_out: typing.Union[BridgeChatOut, bool] = True
|
|
||||||
|
|
||||||
|
|
||||||
class FactorioWeb(WebWorld):
|
class FactorioWeb(WebWorld):
|
||||||
@@ -115,6 +92,7 @@ class Factorio(World):
|
|||||||
settings: typing.ClassVar[FactorioSettings]
|
settings: typing.ClassVar[FactorioSettings]
|
||||||
trap_names: tuple[str] = ("Evolution", "Attack", "Teleport", "Grenade", "Cluster Grenade", "Artillery",
|
trap_names: tuple[str] = ("Evolution", "Attack", "Teleport", "Grenade", "Cluster Grenade", "Artillery",
|
||||||
"Atomic Rocket", "Atomic Cliff Remover", "Inventory Spill")
|
"Atomic Rocket", "Atomic Cliff Remover", "Inventory Spill")
|
||||||
|
want_progressives: dict[str, bool] = collections.defaultdict(lambda: False)
|
||||||
|
|
||||||
def __init__(self, world, player: int):
|
def __init__(self, world, player: int):
|
||||||
super(Factorio, self).__init__(world, player)
|
super(Factorio, self).__init__(world, player)
|
||||||
@@ -133,6 +111,8 @@ class Factorio(World):
|
|||||||
self.options.max_tech_cost.value, self.options.min_tech_cost.value
|
self.options.max_tech_cost.value, self.options.min_tech_cost.value
|
||||||
self.tech_mix = self.options.tech_cost_mix.value
|
self.tech_mix = self.options.tech_cost_mix.value
|
||||||
self.skip_silo = self.options.silo.value == Silo.option_spawn
|
self.skip_silo = self.options.silo.value == Silo.option_spawn
|
||||||
|
self.want_progressives = collections.defaultdict(
|
||||||
|
lambda: self.options.progressive.want_progressives(self.random))
|
||||||
|
|
||||||
def create_regions(self):
|
def create_regions(self):
|
||||||
player = self.player
|
player = self.player
|
||||||
@@ -201,9 +181,6 @@ class Factorio(World):
|
|||||||
range(getattr(self.options,
|
range(getattr(self.options,
|
||||||
f"{trap_name.lower().replace(' ', '_')}_traps")))
|
f"{trap_name.lower().replace(' ', '_')}_traps")))
|
||||||
|
|
||||||
want_progressives = collections.defaultdict(lambda: self.options.progressive.
|
|
||||||
want_progressives(self.random))
|
|
||||||
|
|
||||||
cost_sorted_locations = sorted(self.science_locations, key=lambda location: location.name)
|
cost_sorted_locations = sorted(self.science_locations, key=lambda location: location.name)
|
||||||
special_index = {"automation": 0,
|
special_index = {"automation": 0,
|
||||||
"logistics": 1,
|
"logistics": 1,
|
||||||
@@ -218,7 +195,7 @@ class Factorio(World):
|
|||||||
for tech_name in base_tech_table:
|
for tech_name in base_tech_table:
|
||||||
if tech_name not in self.removed_technologies:
|
if tech_name not in self.removed_technologies:
|
||||||
progressive_item_name = tech_to_progressive_lookup.get(tech_name, tech_name)
|
progressive_item_name = tech_to_progressive_lookup.get(tech_name, tech_name)
|
||||||
want_progressive = want_progressives[progressive_item_name]
|
want_progressive = self.want_progressives[progressive_item_name]
|
||||||
item_name = progressive_item_name if want_progressive else tech_name
|
item_name = progressive_item_name if want_progressive else tech_name
|
||||||
tech_item = self.create_item(item_name)
|
tech_item = self.create_item(item_name)
|
||||||
index = special_index.get(tech_name, None)
|
index = special_index.get(tech_name, None)
|
||||||
@@ -233,6 +210,12 @@ class Factorio(World):
|
|||||||
loc.place_locked_item(tech_item)
|
loc.place_locked_item(tech_item)
|
||||||
loc.revealed = True
|
loc.revealed = True
|
||||||
|
|
||||||
|
def get_filler_item_name(self) -> str:
|
||||||
|
tech_name: str = self.random.choice(tuple(tech_table))
|
||||||
|
progressive_item_name: str = tech_to_progressive_lookup.get(tech_name, tech_name)
|
||||||
|
want_progressive: bool = self.want_progressives[progressive_item_name]
|
||||||
|
return progressive_item_name if want_progressive else tech_name
|
||||||
|
|
||||||
def set_rules(self):
|
def set_rules(self):
|
||||||
player = self.player
|
player = self.player
|
||||||
shapes = get_shapes(self)
|
shapes = get_shapes(self)
|
||||||
|
|||||||
26
worlds/factorio/settings.py
Normal file
26
worlds/factorio/settings.py
Normal file
@@ -0,0 +1,26 @@
|
|||||||
|
import typing
|
||||||
|
|
||||||
|
import settings
|
||||||
|
|
||||||
|
|
||||||
|
class FactorioSettings(settings.Group):
|
||||||
|
class Executable(settings.UserFilePath):
|
||||||
|
is_exe = True
|
||||||
|
|
||||||
|
class ServerSettings(settings.OptionalUserFilePath):
|
||||||
|
"""
|
||||||
|
by default, no settings are loaded if this file does not exist. \
|
||||||
|
If this file does exist, then it will be used.
|
||||||
|
server_settings: "factorio\\\\data\\\\server-settings.json"
|
||||||
|
"""
|
||||||
|
|
||||||
|
class FilterItemSends(settings.Bool):
|
||||||
|
"""Whether to filter item send messages displayed in-game to only those that involve you."""
|
||||||
|
|
||||||
|
class BridgeChatOut(settings.Bool):
|
||||||
|
"""Whether to send chat messages from players on the Factorio server to Archipelago."""
|
||||||
|
|
||||||
|
executable: Executable = Executable("factorio/bin/x64/factorio")
|
||||||
|
server_settings: typing.Optional[ServerSettings] = None
|
||||||
|
filter_item_sends: typing.Union[FilterItemSends, bool] = False
|
||||||
|
bridge_chat_out: typing.Union[BridgeChatOut, bool] = True
|
||||||
@@ -278,7 +278,7 @@ one file, removing the need to manage separate files if one chooses to do so.
|
|||||||
As a precautionary measure, before submitting a multi-game yaml like this one in a synchronous/sync multiworld, please
|
As a precautionary measure, before submitting a multi-game yaml like this one in a synchronous/sync multiworld, please
|
||||||
confirm that the other players in the multi are OK with what you are submitting, and please be fairly reasonable about
|
confirm that the other players in the multi are OK with what you are submitting, and please be fairly reasonable about
|
||||||
the submission. (i.e. Multiple long games (SMZ3, OoT, HK, etc.) for a game intended to be <2 hrs is not likely considered
|
the submission. (i.e. Multiple long games (SMZ3, OoT, HK, etc.) for a game intended to be <2 hrs is not likely considered
|
||||||
reasonable, but submitting a ChecksFinder alongside another game OR submitting multiple Slay the Spire runs is likely
|
reasonable, but submitting a ChecksFinder alongside another game is likely
|
||||||
OK)
|
OK)
|
||||||
|
|
||||||
To configure your file to generate multiple worlds, use 3 dashes `---` on an empty line to separate the ending of one
|
To configure your file to generate multiple worlds, use 3 dashes `---` on an empty line to separate the ending of one
|
||||||
@@ -335,7 +335,7 @@ Minecraft:
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
description: Example of generating multiple worlds. World 3 of 3
|
description: Example of generating multiple worlds. World 2 of 2
|
||||||
name: ExampleFinder
|
name: ExampleFinder
|
||||||
game: ChecksFinder
|
game: ChecksFinder
|
||||||
|
|
||||||
|
|||||||
@@ -20,9 +20,11 @@ It is generally recommended that you use a virtual environment to run python bas
|
|||||||
3. Run the command `source venv/bin/activate` to activate the virtual environment.
|
3. Run the command `source venv/bin/activate` to activate the virtual environment.
|
||||||
4. If you want to exit the virtual environment, run the command `deactivate`.
|
4. If you want to exit the virtual environment, run the command `deactivate`.
|
||||||
## Steps to Run the Clients
|
## Steps to Run the Clients
|
||||||
1. If your game doesn't have a patch file, run the command `python3 SNIClient.py`, changing the filename with the file of the client you want to run.
|
1. Run the command `python3 Launcher.py`.
|
||||||
2. If your game does have a patch file, move the base rom to the Archipelago directory and run the command `python3 SNIClient.py 'patchfile'` with the filename extension for the patch file (apsm, aplttp, apsmz3, etc.) included and changing the filename with the file of the client you want to run.
|
2. If your game doesn't have a patch file, just click the desired client in the right side column.
|
||||||
3. Your client should now be running and rom created (where applicable).
|
3. If your game does have a patch file, click the 'Open Patch' button and navigate to your patch file (the filename extension will look something like apsm, aplttp, apsmz3, etc.).
|
||||||
|
4. If the patching process needs a rom, but cannot find it, it will ask you to navigate to your legally obtained rom.
|
||||||
|
5. Your client should now be running and rom created (where applicable).
|
||||||
## Additional Steps for SNES Games
|
## Additional Steps for SNES Games
|
||||||
1. If using RetroArch, the instructions to set up your emulator [here in the Link to the Past setup guide](https://archipelago.gg/tutorial/A%20Link%20to%20the%20Past/multiworld/en) also work on the macOS version of RetroArch.
|
1. If using RetroArch, the instructions to set up your emulator [here in the Link to the Past setup guide](https://archipelago.gg/tutorial/A%20Link%20to%20the%20Past/multiworld/en) also work on the macOS version of RetroArch.
|
||||||
2. Double click on the SNI tar.gz download to extract the files to an SNI directory. If it isn't already, rename this directory to SNI to make some steps easier.
|
2. Double click on the SNI tar.gz download to extract the files to an SNI directory. If it isn't already, rename this directory to SNI to make some steps easier.
|
||||||
|
|||||||
@@ -104,15 +104,7 @@ A list of all available items and locations can be found in the [website's datap
|
|||||||
- Spirit Temple Silver Gauntlets Chest
|
- Spirit Temple Silver Gauntlets Chest
|
||||||
world: false
|
world: false
|
||||||
|
|
||||||
# example block 3 - Slay the Spire
|
# example block 3 - Factorio
|
||||||
- items:
|
|
||||||
Boss Relic: 3
|
|
||||||
locations:
|
|
||||||
- Boss Relic 1
|
|
||||||
- Boss Relic 2
|
|
||||||
- Boss Relic 3
|
|
||||||
|
|
||||||
# example block 4 - Factorio
|
|
||||||
- items:
|
- items:
|
||||||
progressive-electric-energy-distribution: 2
|
progressive-electric-energy-distribution: 2
|
||||||
electric-energy-accumulators: 1
|
electric-energy-accumulators: 1
|
||||||
@@ -125,7 +117,7 @@ A list of all available items and locations can be found in the [website's datap
|
|||||||
percentage: 80
|
percentage: 80
|
||||||
force: true
|
force: true
|
||||||
|
|
||||||
# example block 5 - Secret of Evermore
|
# example block 4 - Secret of Evermore
|
||||||
- items:
|
- items:
|
||||||
Levitate: 1
|
Levitate: 1
|
||||||
Revealer: 1
|
Revealer: 1
|
||||||
@@ -136,7 +128,7 @@ A list of all available items and locations can be found in the [website's datap
|
|||||||
world: true
|
world: true
|
||||||
count: 2
|
count: 2
|
||||||
|
|
||||||
# example block 6 - A Link to the Past
|
# example block 5 - A Link to the Past
|
||||||
- items:
|
- items:
|
||||||
Progressive Sword: 4
|
Progressive Sword: 4
|
||||||
world:
|
world:
|
||||||
@@ -150,12 +142,11 @@ A list of all available items and locations can be found in the [website's datap
|
|||||||
player's Starter Chest 1 and removes the chosen item from the item pool.
|
player's Starter Chest 1 and removes the chosen item from the item pool.
|
||||||
2. This block will always trigger and will place the player's swords, bow, magic meter, strength upgrades, and hookshots
|
2. This block will always trigger and will place the player's swords, bow, magic meter, strength upgrades, and hookshots
|
||||||
in their own dungeon major item chests.
|
in their own dungeon major item chests.
|
||||||
3. This block will always trigger and will lock boss relics on the bosses.
|
3. This block has an 80% chance of occurring, and when it does, it will place all but 1 of the items randomly among the
|
||||||
4. This block has an 80% chance of occurring, and when it does, it will place all but 1 of the items randomly among the
|
|
||||||
four locations chosen here.
|
four locations chosen here.
|
||||||
5. This block will always trigger and will attempt to place a random 2 of Levitate, Revealer and Energize into
|
4. This block will always trigger and will attempt to place a random 2 of Levitate, Revealer and Energize into
|
||||||
other players' Master Sword Pedestals or Boss Relic 1 locations.
|
other players' Master Sword Pedestals or Boss Relic 1 locations.
|
||||||
6. This block will always trigger and will attempt to place a random number, between 1 and 4, of progressive swords
|
5. This block will always trigger and will attempt to place a random number, between 1 and 4, of progressive swords
|
||||||
into any locations within the game slots named BobsSlaytheSpire and BobsRogueLegacy.
|
into any locations within the game slots named BobsSlaytheSpire and BobsRogueLegacy.
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -3,34 +3,34 @@
|
|||||||
## Required Software
|
## Required Software
|
||||||
* Download and unzip the Lumafly Mod Manager from the [Lumafly website](https://themulhima.github.io/Lumafly/).
|
* Download and unzip the Lumafly Mod Manager from the [Lumafly website](https://themulhima.github.io/Lumafly/).
|
||||||
* A legal copy of Hollow Knight.
|
* A legal copy of Hollow Knight.
|
||||||
* Steam, Gog, and Xbox Game Pass versions of the game are supported.
|
* Steam, Gog, and Xbox Game Pass versions of the game are supported.
|
||||||
* Windows, Mac, and Linux (including Steam Deck) are supported.
|
* Windows, Mac, and Linux (including Steam Deck) are supported.
|
||||||
|
|
||||||
## Installing the Archipelago Mod using Lumafly
|
## Installing the Archipelago Mod using Lumafly
|
||||||
1. Launch Lumafly and ensure it locates your Hollow Knight installation directory.
|
1. Launch Lumafly and ensure it locates your Hollow Knight installation directory.
|
||||||
2. Install the Archipelago mods by doing either of the following:
|
2. Install the Archipelago mods by doing either of the following:
|
||||||
* Click one of the links below to allow Lumafly to install the mods. Lumafly will prompt for confirmation.
|
* Click one of the links below to allow Lumafly to install the mods. Lumafly will prompt for confirmation.
|
||||||
* [Archipelago and dependencies only](https://themulhima.github.io/Lumafly/commands/download/?mods=Archipelago)
|
* [Archipelago and dependencies only](https://themulhima.github.io/Lumafly/commands/download/?mods=Archipelago)
|
||||||
* [Archipelago with rando essentials](https://themulhima.github.io/Lumafly/commands/download/?mods=Archipelago/Archipelago%20Map%20Mod/RecentItemsDisplay/DebugMod/RandoStats/Additional%20Timelines/CompassAlwaysOn/AdditionalMaps/)
|
* [Archipelago with rando essentials](https://themulhima.github.io/Lumafly/commands/download/?mods=Archipelago/Archipelago%20Map%20Mod/RecentItemsDisplay/DebugMod/RandoStats/Additional%20Timelines/CompassAlwaysOn/AdditionalMaps/)
|
||||||
(includes Archipelago Map Mod, RecentItemsDisplay, DebugMod, RandoStats, AdditionalTimelines, CompassAlwaysOn,
|
(includes Archipelago Map Mod, RecentItemsDisplay, DebugMod, RandoStats, AdditionalTimelines, CompassAlwaysOn,
|
||||||
and AdditionalMaps).
|
and AdditionalMaps).
|
||||||
* Click the "Install" button near the "Archipelago" mod entry. If desired, also install "Archipelago Map Mod"
|
* Click the "Install" button near the "Archipelago" mod entry. If desired, also install "Archipelago Map Mod"
|
||||||
to use as an in-game tracker.
|
to use as an in-game tracker.
|
||||||
3. Launch the game, you're all set!
|
3. Launch the game, you're all set!
|
||||||
|
|
||||||
### What to do if Lumafly fails to find your installation directory
|
### What to do if Lumafly fails to find your installation directory
|
||||||
1. Find the directory manually.
|
1. Find the directory manually.
|
||||||
* Xbox Game Pass:
|
* Xbox Game Pass:
|
||||||
1. Enter the Xbox app and move your mouse over "Hollow Knight" on the left sidebar.
|
1. Enter the Xbox app and move your mouse over "Hollow Knight" on the left sidebar.
|
||||||
2. Click the three points then click "Manage".
|
2. Click the three points then click "Manage".
|
||||||
3. Go to the "Files" tab and select "Browse...".
|
3. Go to the "Files" tab and select "Browse...".
|
||||||
4. Click "Hollow Knight", then "Content", then click the path bar and copy it.
|
4. Click "Hollow Knight", then "Content", then click the path bar and copy it.
|
||||||
* Steam:
|
* Steam:
|
||||||
1. You likely put your Steam library in a non-standard place. If this is the case, you probably know where
|
1. You likely put your Steam library in a non-standard place. If this is the case, you probably know where
|
||||||
it is. Find your steam library and then find the Hollow Knight folder and copy the path.
|
it is. Find your steam library and then find the Hollow Knight folder and copy the path.
|
||||||
* Windows - `C:\Program Files (x86)\Steam\steamapps\common\Hollow Knight`
|
* Windows - `C:\Program Files (x86)\Steam\steamapps\common\Hollow Knight`
|
||||||
* Linux/Steam Deck - ~/.local/share/Steam/steamapps/common/Hollow Knight
|
* Linux/Steam Deck - ~/.local/share/Steam/steamapps/common/Hollow Knight
|
||||||
* Mac - ~/Library/Application Support/Steam/steamapps/common/Hollow Knight/hollow_knight.app
|
* Mac - ~/Library/Application Support/Steam/steamapps/common/Hollow Knight/hollow_knight.app
|
||||||
2. Run Lumafly as an administrator and, when it asks you for the path, paste the path you copied.
|
2. Run Lumafly as an administrator and, when it asks you for the path, paste the path you copied.
|
||||||
|
|
||||||
## Configuring your YAML File
|
## Configuring your YAML File
|
||||||
@@ -49,8 +49,8 @@ website to generate a YAML using a graphical interface.
|
|||||||
4. Enter the correct settings for your Archipelago server.
|
4. Enter the correct settings for your Archipelago server.
|
||||||
5. Hit **Start** to begin the game. The game will stall for a few seconds while it does all item placements.
|
5. Hit **Start** to begin the game. The game will stall for a few seconds while it does all item placements.
|
||||||
6. The game will immediately drop you into the randomized game.
|
6. The game will immediately drop you into the randomized game.
|
||||||
* If you are waiting for a countdown then wait for it to lapse before hitting Start.
|
* If you are waiting for a countdown then wait for it to lapse before hitting Start.
|
||||||
* Or hit Start then pause the game once you're in it.
|
* Or hit Start then pause the game once you're in it.
|
||||||
|
|
||||||
## Hints and other commands
|
## Hints and other commands
|
||||||
While playing in a multiworld, you can interact with the server using various commands listed in the
|
While playing in a multiworld, you can interact with the server using various commands listed in the
|
||||||
|
|||||||
@@ -3,28 +3,28 @@
|
|||||||
## Programas obrigatórios
|
## Programas obrigatórios
|
||||||
* Baixe e extraia o Lumafly Mod Manager (gerenciador de mods Lumafly) do [Site Lumafly](https://themulhima.github.io/Lumafly/).
|
* Baixe e extraia o Lumafly Mod Manager (gerenciador de mods Lumafly) do [Site Lumafly](https://themulhima.github.io/Lumafly/).
|
||||||
* Uma cópia legal de Hollow Knight.
|
* Uma cópia legal de Hollow Knight.
|
||||||
* Versões Steam, Gog, e Xbox Game Pass do jogo são suportadas.
|
* Versões Steam, Gog, e Xbox Game Pass do jogo são suportadas.
|
||||||
* Windows, Mac, e Linux (incluindo Steam Deck) são suportados.
|
* Windows, Mac, e Linux (incluindo Steam Deck) são suportados.
|
||||||
|
|
||||||
## Instalando o mod Archipelago Mod usando Lumafly
|
## Instalando o mod Archipelago Mod usando Lumafly
|
||||||
1. Abra o Lumafly e confirme que ele localizou sua pasta de instalação do Hollow Knight.
|
1. Abra o Lumafly e confirme que ele localizou sua pasta de instalação do Hollow Knight.
|
||||||
2. Clique em "Install (instalar)" perto da opção "Archipelago" mod.
|
2. Clique em "Install (instalar)" perto da opção "Archipelago" mod.
|
||||||
* Se quiser, instale também o "Archipelago Map Mod (mod do mapa do archipelago)" para usá-lo como rastreador dentro do jogo.
|
* Se quiser, instale também o "Archipelago Map Mod (mod do mapa do archipelago)" para usá-lo como rastreador dentro do jogo.
|
||||||
3. Abra o jogo, tudo preparado!
|
3. Abra o jogo, tudo preparado!
|
||||||
|
|
||||||
### O que fazer se o Lumafly falha em encontrar a sua pasta de instalação
|
### O que fazer se o Lumafly falha em encontrar a sua pasta de instalação
|
||||||
1. Encontre a pasta manualmente.
|
1. Encontre a pasta manualmente.
|
||||||
* Xbox Game Pass:
|
* Xbox Game Pass:
|
||||||
1. Entre no seu aplicativo Xbox e mova seu mouse em cima de "Hollow Knight" na sua barra da esquerda.
|
1. Entre no seu aplicativo Xbox e mova seu mouse em cima de "Hollow Knight" na sua barra da esquerda.
|
||||||
2. Clique nos 3 pontos depois clique gerenciar.
|
2. Clique nos 3 pontos depois clique gerenciar.
|
||||||
3. Vá nos arquivos e selecione procurar.
|
3. Vá nos arquivos e selecione procurar.
|
||||||
4. Clique em "Hollow Knight", depois em "Content (Conteúdo)", depois clique na barra com o endereço e a copie.
|
4. Clique em "Hollow Knight", depois em "Content (Conteúdo)", depois clique na barra com o endereço e a copie.
|
||||||
* Steam:
|
* Steam:
|
||||||
1. Você provavelmente colocou sua biblioteca Steam num local não padrão. Se esse for o caso você provavelmente sabe onde está.
|
1. Você provavelmente colocou sua biblioteca Steam num local não padrão. Se esse for o caso você provavelmente sabe onde está.
|
||||||
. Encontre sua biblioteca Steam, depois encontre a pasta do Hollow Knight e copie seu endereço.
|
Encontre sua biblioteca Steam, depois encontre a pasta do Hollow Knight e copie seu endereço.
|
||||||
* Windows - `C:\Program Files (x86)\Steam\steamapps\common\Hollow Knight`
|
* Windows - `C:\Program Files (x86)\Steam\steamapps\common\Hollow Knight`
|
||||||
* Linux/Steam Deck - `~/.local/share/Steam/steamapps/common/Hollow Knight`
|
* Linux/Steam Deck - `~/.local/share/Steam/steamapps/common/Hollow Knight`
|
||||||
* Mac - `~/Library/Application Support/Steam/steamapps/common/Hollow Knight/hollow_knight.app`
|
* Mac - `~/Library/Application Support/Steam/steamapps/common/Hollow Knight/hollow_knight.app`
|
||||||
2. Rode o Lumafly como administrador e, quando ele perguntar pelo endereço do arquivo, cole o endereço do arquivo que você copiou.
|
2. Rode o Lumafly como administrador e, quando ele perguntar pelo endereço do arquivo, cole o endereço do arquivo que você copiou.
|
||||||
|
|
||||||
## Configurando seu arquivo YAML
|
## Configurando seu arquivo YAML
|
||||||
@@ -43,8 +43,8 @@ para gerar o YAML usando a interface gráfica.
|
|||||||
4. Coloque as configurações corretas do seu servidor Archipelago.
|
4. Coloque as configurações corretas do seu servidor Archipelago.
|
||||||
5. Aperte em **Começar**. O jogo vai travar por uns segundos enquanto ele coloca todos itens.
|
5. Aperte em **Começar**. O jogo vai travar por uns segundos enquanto ele coloca todos itens.
|
||||||
6. O jogo vai te colocar imediatamente numa partida randomizada.
|
6. O jogo vai te colocar imediatamente numa partida randomizada.
|
||||||
* Se você está esperando uma contagem então espere ele cair antes de apertar começar.
|
* Se você está esperando uma contagem então espere ele cair antes de apertar começar.
|
||||||
* Ou clique em começar e pause o jogo enquanto estiver nele.
|
* Ou clique em começar e pause o jogo enquanto estiver nele.
|
||||||
|
|
||||||
## Dicas e outros comandos
|
## Dicas e outros comandos
|
||||||
Enquanto jogar um multiworld, você pode interagir com o servidor usando vários comandos listados no
|
Enquanto jogar um multiworld, você pode interagir com o servidor usando vários comandos listados no
|
||||||
|
|||||||
@@ -10,7 +10,7 @@
|
|||||||
Kingdom Hearts II Final Mix from the [Epic Games Store](https://store.epicgames.com/en-US/discover/kingdom-hearts) or [Steam](https://store.steampowered.com/app/2552430/KINGDOM_HEARTS_HD_1525_ReMIX/)
|
Kingdom Hearts II Final Mix from the [Epic Games Store](https://store.epicgames.com/en-US/discover/kingdom-hearts) or [Steam](https://store.steampowered.com/app/2552430/KINGDOM_HEARTS_HD_1525_ReMIX/)
|
||||||
|
|
||||||
- Follow this Guide to set up these requirements [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/)
|
- Follow this Guide to set up these requirements [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/)
|
||||||
1. Version 25.01.26.0 or greater OpenKH Mod Manager with Panacea
|
1. Version 25.03.16.0 or greater OpenKH Mod Manager with Panacea
|
||||||
2. Lua Backend from the OpenKH Mod Manager
|
2. Lua Backend from the OpenKH Mod Manager
|
||||||
3. Install the mod `KH2FM-Mods-Num/GoA-ROM-Edition` using OpenKH Mod Manager
|
3. Install the mod `KH2FM-Mods-Num/GoA-ROM-Edition` using OpenKH Mod Manager
|
||||||
- Needed for Archipelago
|
- Needed for Archipelago
|
||||||
@@ -27,7 +27,7 @@ Kingdom Hearts II Final Mix from the [Epic Games Store](https://store.epicgames.
|
|||||||
|
|
||||||
Load this mod just like the <b>GoA ROM</b> you did during the KH2 Rando setup. `JaredWeakStrike/APCompanion`<br>
|
Load this mod just like the <b>GoA ROM</b> you did during the KH2 Rando setup. `JaredWeakStrike/APCompanion`<br>
|
||||||
Have this mod second-highest priority below the .zip seed.<br>
|
Have this mod second-highest priority below the .zip seed.<br>
|
||||||
This mod is based upon Num's Garden of Assemblege Mod and requires it to work. Without Num this could not be possible.
|
This mod is based upon Num's Garden of Assemblage Mod and requires it to work. Without Num this could not be possible.
|
||||||
|
|
||||||
<h3 style="text-transform:none";>Required: Auto Save Mod and KH2 Lua Library</h3>
|
<h3 style="text-transform:none";>Required: Auto Save Mod and KH2 Lua Library</h3>
|
||||||
|
|
||||||
@@ -35,7 +35,7 @@ Load these mods just like you loaded the GoA ROM mod during the KH2 Rando setup.
|
|||||||
|
|
||||||
<h3 style="text-transform:none";>Optional QoL Mods: AP QoL and Bear Skip</h3>
|
<h3 style="text-transform:none";>Optional QoL Mods: AP QoL and Bear Skip</h3>
|
||||||
|
|
||||||
`JaredWeakStrike/AP_QOL` Makes the urns minigames much faster, makes Cavern of Remembrance orbs drop significantly more drive orbs for refilling drive/leveling master form, skips the animation when using the bulky vendor RC, skips carpet escape auto scroller in Agrabah 2, and prevents the wardrobe in the Beasts Castle wardrobe push minigame from waking up while being pushed.
|
`JaredWeakStrike/AP_QOL` Makes the urns minigames much faster, makes Cavern of Remembrance orbs drop significantly more drive orbs for refilling drive/leveling master form, skips the animation when using the bulky vendor RC, skips carpet escape auto-scroller in Agrabah 2, and prevents the wardrobe in the Beasts Castle wardrobe push minigame from waking up while being pushed.
|
||||||
|
|
||||||
`shananas/BearSkip` Skips all minigames in 100 Acre Woods except the Spooky Cave minigame since there are chests in Spooky Cave you can only get during the minigame. For Spooky Cave, Pooh is moved to the other side of the invisible wall that prevents you from using his RC to finish the minigame.
|
`shananas/BearSkip` Skips all minigames in 100 Acre Woods except the Spooky Cave minigame since there are chests in Spooky Cave you can only get during the minigame. For Spooky Cave, Pooh is moved to the other side of the invisible wall that prevents you from using his RC to finish the minigame.
|
||||||
|
|
||||||
@@ -83,6 +83,9 @@ Enter The room's port number into the top box <b> where the x's are</b> and pres
|
|||||||
- Loading into Simulated Twilight Town Instead of the GOA.
|
- Loading into Simulated Twilight Town Instead of the GOA.
|
||||||
- To fix this look over the guide at [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/). Specifically the Panacea and Lua Backend Steps.
|
- To fix this look over the guide at [KH2Rando.com](https://tommadness.github.io/KH2Randomizer/setup/Panacea-ModLoader/). Specifically the Panacea and Lua Backend Steps.
|
||||||
|
|
||||||
|
- Using a seed from the standalone KH2 Randomizer Seed Generator.
|
||||||
|
- The Archipelago version of the KH2 Randomizer does not use this Seed Generator; refer to the [Archipelago Setup](https://archipelago.gg/tutorial/Archipelago/setup/en) to learn how to generate and play a seed through Archipelago.
|
||||||
|
|
||||||
<h2 style="text-transform:none"; >Best Practices</h2>
|
<h2 style="text-transform:none"; >Best Practices</h2>
|
||||||
|
|
||||||
- Make a save at the start of the GoA before opening anything. This will be the file to select when loading an autosave if/when your game crashes.
|
- Make a save at the start of the GoA before opening anything. This will be the file to select when loading an autosave if/when your game crashes.
|
||||||
@@ -139,4 +142,4 @@ This pack will handle logic, received items, checked locations and autotabbing f
|
|||||||
- Why should I install the auto save mod at `KH2FM-Mods-equations19/auto-save` and `KH2FM-Mods-equations19/KH2-Lua-Library`?
|
- Why should I install the auto save mod at `KH2FM-Mods-equations19/auto-save` and `KH2FM-Mods-equations19/KH2-Lua-Library`?
|
||||||
- Because Kingdom Hearts 2 is prone to crashes and will keep you from losing your progress. Both mods are needed for auto save to work.
|
- Because Kingdom Hearts 2 is prone to crashes and will keep you from losing your progress. Both mods are needed for auto save to work.
|
||||||
- How do I load an auto save?
|
- How do I load an auto save?
|
||||||
- To load an auto-save, hold down the Select or your equivalent on your prefered controller while choosing a file. Make sure to hold the button down the whole time.
|
- To load an auto-save, hold down the Select or your equivalent on your preferred controller while choosing a file. Make sure to hold the button down the whole time.
|
||||||
|
|||||||
@@ -151,8 +151,7 @@ class ItemTracker:
|
|||||||
def __init__(self, gameboy) -> None:
|
def __init__(self, gameboy) -> None:
|
||||||
self.gameboy = gameboy
|
self.gameboy = gameboy
|
||||||
self.loadItems()
|
self.loadItems()
|
||||||
pass
|
self.extraItems = {}
|
||||||
extraItems = {}
|
|
||||||
|
|
||||||
async def readRamByte(self, byte):
|
async def readRamByte(self, byte):
|
||||||
return (await self.gameboy.read_memory_cache([byte]))[byte]
|
return (await self.gameboy.read_memory_cache([byte]))[byte]
|
||||||
|
|||||||
@@ -3,7 +3,7 @@ Archipelago init file for Lingo
|
|||||||
"""
|
"""
|
||||||
from logging import warning
|
from logging import warning
|
||||||
|
|
||||||
from BaseClasses import CollectionState, Item, ItemClassification, Tutorial
|
from BaseClasses import CollectionState, Item, ItemClassification, Tutorial, Location, LocationProgressType
|
||||||
from Options import OptionError
|
from Options import OptionError
|
||||||
from worlds.AutoWorld import WebWorld, World
|
from worlds.AutoWorld import WebWorld, World
|
||||||
from .datatypes import Room, RoomEntrance
|
from .datatypes import Room, RoomEntrance
|
||||||
@@ -80,10 +80,6 @@ class LingoWorld(World):
|
|||||||
for item in self.player_logic.real_items:
|
for item in self.player_logic.real_items:
|
||||||
state.collect(self.create_item(item), True)
|
state.collect(self.create_item(item), True)
|
||||||
|
|
||||||
# Exception to the above: a forced good item is not considered a "real item", but needs to be here anyway.
|
|
||||||
if self.player_logic.forced_good_item != "":
|
|
||||||
state.collect(self.create_item(self.player_logic.forced_good_item), True)
|
|
||||||
|
|
||||||
all_locations = self.multiworld.get_locations(self.player)
|
all_locations = self.multiworld.get_locations(self.player)
|
||||||
state.sweep_for_advancements(locations=all_locations)
|
state.sweep_for_advancements(locations=all_locations)
|
||||||
|
|
||||||
@@ -105,11 +101,6 @@ class LingoWorld(World):
|
|||||||
def create_items(self):
|
def create_items(self):
|
||||||
pool = [self.create_item(name) for name in self.player_logic.real_items]
|
pool = [self.create_item(name) for name in self.player_logic.real_items]
|
||||||
|
|
||||||
if self.player_logic.forced_good_item != "":
|
|
||||||
new_item = self.create_item(self.player_logic.forced_good_item)
|
|
||||||
location_obj = self.multiworld.get_location("Second Room - Good Luck", self.player)
|
|
||||||
location_obj.place_locked_item(new_item)
|
|
||||||
|
|
||||||
item_difference = len(self.player_logic.real_locations) - len(pool)
|
item_difference = len(self.player_logic.real_locations) - len(pool)
|
||||||
if item_difference:
|
if item_difference:
|
||||||
trap_percentage = self.options.trap_percentage
|
trap_percentage = self.options.trap_percentage
|
||||||
@@ -169,6 +160,30 @@ class LingoWorld(World):
|
|||||||
def set_rules(self):
|
def set_rules(self):
|
||||||
self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
|
self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
|
||||||
|
|
||||||
|
def place_good_item(self, progitempool: list[Item], fill_locations: list[Location]):
|
||||||
|
if len(self.player_logic.good_item_options) == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
good_location = self.get_location("Second Room - Good Luck")
|
||||||
|
if good_location.progress_type == LocationProgressType.EXCLUDED or good_location not in fill_locations:
|
||||||
|
return
|
||||||
|
|
||||||
|
good_items = list(filter(lambda progitem: progitem.player == self.player and
|
||||||
|
progitem.name in self.player_logic.good_item_options, progitempool))
|
||||||
|
|
||||||
|
if len(good_items) == 0:
|
||||||
|
return
|
||||||
|
|
||||||
|
good_item = self.random.choice(good_items)
|
||||||
|
good_location.place_locked_item(good_item)
|
||||||
|
|
||||||
|
progitempool.remove(good_item)
|
||||||
|
fill_locations.remove(good_location)
|
||||||
|
|
||||||
|
def fill_hook(self, progitempool: list[Item], usefulitempool: list[Item], filleritempool: list[Item],
|
||||||
|
fill_locations: list[Location]):
|
||||||
|
self.place_good_item(progitempool, fill_locations)
|
||||||
|
|
||||||
def fill_slot_data(self):
|
def fill_slot_data(self):
|
||||||
slot_options = [
|
slot_options = [
|
||||||
"death_link", "victory_condition", "shuffle_colors", "shuffle_doors", "shuffle_paintings", "shuffle_panels",
|
"death_link", "victory_condition", "shuffle_colors", "shuffle_doors", "shuffle_paintings", "shuffle_panels",
|
||||||
|
|||||||
@@ -2,7 +2,7 @@ from dataclasses import dataclass
|
|||||||
|
|
||||||
from schema import And, Schema
|
from schema import And, Schema
|
||||||
|
|
||||||
from Options import Toggle, Choice, DefaultOnToggle, Range, PerGameCommonOptions, StartInventoryPool, OptionDict, \
|
from Options import Toggle, Choice, DefaultOnToggle, Range, PerGameCommonOptions, StartInventoryPool, OptionCounter, \
|
||||||
OptionGroup
|
OptionGroup
|
||||||
from .items import TRAP_ITEMS
|
from .items import TRAP_ITEMS
|
||||||
|
|
||||||
@@ -222,13 +222,14 @@ class TrapPercentage(Range):
|
|||||||
default = 20
|
default = 20
|
||||||
|
|
||||||
|
|
||||||
class TrapWeights(OptionDict):
|
class TrapWeights(OptionCounter):
|
||||||
"""Specify the distribution of traps that should be placed into the pool.
|
"""Specify the distribution of traps that should be placed into the pool.
|
||||||
|
|
||||||
If you don't want a specific type of trap, set the weight to zero.
|
If you don't want a specific type of trap, set the weight to zero.
|
||||||
"""
|
"""
|
||||||
display_name = "Trap Weights"
|
display_name = "Trap Weights"
|
||||||
schema = Schema({trap_name: And(int, lambda n: n >= 0) for trap_name in TRAP_ITEMS})
|
valid_keys = TRAP_ITEMS
|
||||||
|
min = 0
|
||||||
default = {trap_name: 1 for trap_name in TRAP_ITEMS}
|
default = {trap_name: 1 for trap_name in TRAP_ITEMS}
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -95,7 +95,7 @@ class LingoPlayerLogic:
|
|||||||
|
|
||||||
painting_mapping: Dict[str, str]
|
painting_mapping: Dict[str, str]
|
||||||
|
|
||||||
forced_good_item: str
|
good_item_options: List[str]
|
||||||
|
|
||||||
panel_reqs: Dict[str, Dict[str, AccessRequirements]]
|
panel_reqs: Dict[str, Dict[str, AccessRequirements]]
|
||||||
door_reqs: Dict[str, Dict[str, AccessRequirements]]
|
door_reqs: Dict[str, Dict[str, AccessRequirements]]
|
||||||
@@ -151,7 +151,7 @@ class LingoPlayerLogic:
|
|||||||
self.mastery_location = ""
|
self.mastery_location = ""
|
||||||
self.level_2_location = ""
|
self.level_2_location = ""
|
||||||
self.painting_mapping = {}
|
self.painting_mapping = {}
|
||||||
self.forced_good_item = ""
|
self.good_item_options = []
|
||||||
self.panel_reqs = {}
|
self.panel_reqs = {}
|
||||||
self.door_reqs = {}
|
self.door_reqs = {}
|
||||||
self.mastery_reqs = []
|
self.mastery_reqs = []
|
||||||
@@ -344,23 +344,23 @@ class LingoPlayerLogic:
|
|||||||
|
|
||||||
# Starting Room - Back Right Door gives access to OPEN and DEAD END.
|
# Starting Room - Back Right Door gives access to OPEN and DEAD END.
|
||||||
# Starting Room - Exit Door gives access to OPEN and TRACE.
|
# Starting Room - Exit Door gives access to OPEN and TRACE.
|
||||||
good_item_options: List[str] = ["Starting Room - Back Right Door", "Second Room - Exit Door"]
|
self.good_item_options = ["Starting Room - Back Right Door", "Second Room - Exit Door"]
|
||||||
|
|
||||||
if not color_shuffle:
|
if not color_shuffle:
|
||||||
if not world.options.enable_pilgrimage:
|
if not world.options.enable_pilgrimage:
|
||||||
# HOT CRUST and THIS.
|
# HOT CRUST and THIS.
|
||||||
good_item_options.append("Pilgrim Room - Sun Painting")
|
self.good_item_options.append("Pilgrim Room - Sun Painting")
|
||||||
|
|
||||||
if world.options.group_doors:
|
if world.options.group_doors:
|
||||||
# WELCOME BACK, CLOCKWISE, and DRAWL + RUNS.
|
# WELCOME BACK, CLOCKWISE, and DRAWL + RUNS.
|
||||||
good_item_options.append("Welcome Back Doors")
|
self.good_item_options.append("Welcome Back Doors")
|
||||||
else:
|
else:
|
||||||
# WELCOME BACK and CLOCKWISE.
|
# WELCOME BACK and CLOCKWISE.
|
||||||
good_item_options.append("Welcome Back Area - Shortcut to Starting Room")
|
self.good_item_options.append("Welcome Back Area - Shortcut to Starting Room")
|
||||||
|
|
||||||
if world.options.group_doors:
|
if world.options.group_doors:
|
||||||
# Color hallways access (NOTE: reconsider when sunwarp shuffling exists).
|
# Color hallways access (NOTE: reconsider when sunwarp shuffling exists).
|
||||||
good_item_options.append("Rhyme Room Doors")
|
self.good_item_options.append("Rhyme Room Doors")
|
||||||
|
|
||||||
# When painting shuffle is off, most Starting Room paintings give color hallways access. The Wondrous's
|
# When painting shuffle is off, most Starting Room paintings give color hallways access. The Wondrous's
|
||||||
# painting does not, but it gives access to SHRINK and WELCOME BACK.
|
# painting does not, but it gives access to SHRINK and WELCOME BACK.
|
||||||
@@ -376,30 +376,7 @@ class LingoPlayerLogic:
|
|||||||
continue
|
continue
|
||||||
|
|
||||||
pdoor = DOORS_BY_ROOM[painting_obj.required_door.room][painting_obj.required_door.door]
|
pdoor = DOORS_BY_ROOM[painting_obj.required_door.room][painting_obj.required_door.door]
|
||||||
good_item_options.append(pdoor.item_name)
|
self.good_item_options.append(pdoor.item_name)
|
||||||
|
|
||||||
# Copied from The Witness -- remove any plandoed items from the possible good items set.
|
|
||||||
for v in world.multiworld.plando_items[world.player]:
|
|
||||||
if v.get("from_pool", True):
|
|
||||||
for item_key in {"item", "items"}:
|
|
||||||
if item_key in v:
|
|
||||||
if type(v[item_key]) is str:
|
|
||||||
if v[item_key] in good_item_options:
|
|
||||||
good_item_options.remove(v[item_key])
|
|
||||||
elif type(v[item_key]) is dict:
|
|
||||||
for item, weight in v[item_key].items():
|
|
||||||
if weight and item in good_item_options:
|
|
||||||
good_item_options.remove(item)
|
|
||||||
else:
|
|
||||||
# Other type of iterable
|
|
||||||
for item in v[item_key]:
|
|
||||||
if item in good_item_options:
|
|
||||||
good_item_options.remove(item)
|
|
||||||
|
|
||||||
if len(good_item_options) > 0:
|
|
||||||
self.forced_good_item = world.random.choice(good_item_options)
|
|
||||||
self.real_items.remove(self.forced_good_item)
|
|
||||||
self.real_locations.remove("Second Room - Good Luck")
|
|
||||||
|
|
||||||
def randomize_paintings(self, world: "LingoWorld") -> bool:
|
def randomize_paintings(self, world: "LingoWorld") -> bool:
|
||||||
self.painting_mapping.clear()
|
self.painting_mapping.clear()
|
||||||
|
|||||||
@@ -23,21 +23,12 @@ These steps can also be followed to launch the game and check for mod updates af
|
|||||||
### Manual Installation
|
### Manual Installation
|
||||||
|
|
||||||
1. Download and install Courier Mod Loader using the instructions on the release page
|
1. Download and install Courier Mod Loader using the instructions on the release page
|
||||||
* [Latest release is currently 0.7.1](https://github.com/Brokemia/Courier/releases)
|
* [Latest release is currently 0.7.1](https://github.com/Brokemia/Courier/releases)
|
||||||
2. Download and install the randomizer mod
|
2. Download and install the randomizer mod
|
||||||
1. Download the latest TheMessengerRandomizerAP.zip from
|
1. Download the latest TheMessengerRandomizerAP.zip from
|
||||||
[The Messenger Randomizer Mod AP releases page](https://github.com/alwaysintreble/TheMessengerRandomizerModAP/releases)
|
[The Messenger Randomizer Mod AP releases page](https://github.com/alwaysintreble/TheMessengerRandomizerModAP/releases)
|
||||||
2. Extract the zip file to `TheMessenger/Mods/` of your game's install location
|
2. Extract the zip file to `TheMessenger/Mods/` of your game's install location
|
||||||
* You cannot have both the non-AP randomizer and the AP randomizer installed at the same time
|
* You cannot have both the non-AP randomizer and the AP randomizer installed at the same time
|
||||||
3. Optionally, Backup your save game
|
|
||||||
* On Windows
|
|
||||||
1. Press `Windows Key + R` to open run
|
|
||||||
2. Type `%appdata%` to access AppData
|
|
||||||
3. Navigate to `AppData/locallow/SabotageStudios/The Messenger`
|
|
||||||
4. Rename `SaveGame.txt` to any name of your choice
|
|
||||||
* On Linux
|
|
||||||
1. Navigate to `steamapps/compatdata/764790/pfx/drive_c/users/steamuser/AppData/LocalLow/Sabotage Studio/The Messenger`
|
|
||||||
2. Rename `SaveGame.txt` to any name of your choice
|
|
||||||
|
|
||||||
## Joining a MultiWorld Game
|
## Joining a MultiWorld Game
|
||||||
|
|
||||||
@@ -57,15 +48,15 @@ These steps can also be followed to launch the game and check for mod updates af
|
|||||||
1. Launch the game
|
1. Launch the game
|
||||||
2. Navigate to `Options > Archipelago Options`
|
2. Navigate to `Options > Archipelago Options`
|
||||||
3. Enter connection info using the relevant option buttons
|
3. Enter connection info using the relevant option buttons
|
||||||
* **The game is limited to alphanumerical characters, `.`, and `-`.**
|
* **The game is limited to alphanumerical characters, `.`, and `-`.**
|
||||||
* This defaults to `archipelago.gg` and does not need to be manually changed if connecting to a game hosted on the
|
* This defaults to `archipelago.gg` and does not need to be manually changed if connecting to a game hosted on the
|
||||||
website.
|
website.
|
||||||
* If using a name that cannot be entered in the in game menus, there is a config file (APConfig.toml) in the game
|
* If using a name that cannot be entered in the in game menus, there is a config file (APConfig.toml) in the game
|
||||||
directory. When using this, all connection information must be entered in the file.
|
directory. When using this, all connection information must be entered in the file.
|
||||||
4. Select the `Connect to Archipelago` button
|
4. Select the `Connect to Archipelago` button
|
||||||
5. Navigate to save file selection
|
5. Navigate to save file selection
|
||||||
6. Start a new game
|
6. Start a new game
|
||||||
* If you're already connected, deleting an existing save will not disconnect you and is completely safe.
|
* If you're already connected, deleting an existing save will not disconnect you and is completely safe.
|
||||||
|
|
||||||
## Continuing a MultiWorld Game
|
## Continuing a MultiWorld Game
|
||||||
|
|
||||||
|
|||||||
@@ -215,7 +215,7 @@ def set_rules(world: "MM2World") -> None:
|
|||||||
continue
|
continue
|
||||||
highest, wp = max(zip(weapon_weight.values(), weapon_weight.keys()))
|
highest, wp = max(zip(weapon_weight.values(), weapon_weight.keys()))
|
||||||
uses = weapon_energy[wp] // weapon_costs[wp]
|
uses = weapon_energy[wp] // weapon_costs[wp]
|
||||||
if int(uses * boss_damage[wp]) > boss_health[boss]:
|
if int(uses * boss_damage[wp]) >= boss_health[boss]:
|
||||||
used = ceil(boss_health[boss] / boss_damage[wp])
|
used = ceil(boss_health[boss] / boss_damage[wp])
|
||||||
weapon_energy[wp] -= weapon_costs[wp] * used
|
weapon_energy[wp] -= weapon_costs[wp] * used
|
||||||
boss_health[boss] = 0
|
boss_health[boss] = 0
|
||||||
@@ -226,7 +226,7 @@ def set_rules(world: "MM2World") -> None:
|
|||||||
# it should be impossible to be out of energy, simply because even if every boss took 1 from
|
# it should be impossible to be out of energy, simply because even if every boss took 1 from
|
||||||
# Quick Boomerang and no other, it would only be 28 off from defeating all 9,
|
# Quick Boomerang and no other, it would only be 28 off from defeating all 9,
|
||||||
# which Metal Blade should be able to cover
|
# which Metal Blade should be able to cover
|
||||||
wp, max_uses = max((weapon, weapon_energy[weapon] // weapon_costs[weapon])
|
max_uses, wp = max((weapon_energy[weapon] // weapon_costs[weapon], weapon)
|
||||||
for weapon in weapon_weight
|
for weapon in weapon_weight
|
||||||
if weapon != 0 and (weapon != 8 or boss != 12))
|
if weapon != 0 and (weapon != 8 or boss != 12))
|
||||||
# Wily Machine cannot under any circumstances take damage from Time Stopper, prevent this
|
# Wily Machine cannot under any circumstances take damage from Time Stopper, prevent this
|
||||||
|
|||||||
@@ -2,9 +2,9 @@ from math import ceil
|
|||||||
|
|
||||||
from . import MM2TestBase
|
from . import MM2TestBase
|
||||||
from ..options import bosses
|
from ..options import bosses
|
||||||
|
from ..rules import minimum_weakness_requirement
|
||||||
|
|
||||||
|
|
||||||
# Need to figure out how this test should work
|
|
||||||
def validate_wily_5(base: MM2TestBase) -> None:
|
def validate_wily_5(base: MM2TestBase) -> None:
|
||||||
world = base.multiworld.worlds[base.player]
|
world = base.multiworld.worlds[base.player]
|
||||||
weapon_damage = world.weapon_damage
|
weapon_damage = world.weapon_damage
|
||||||
@@ -67,38 +67,54 @@ def validate_wily_5(base: MM2TestBase) -> None:
|
|||||||
weapon_weight.pop(wp)
|
weapon_weight.pop(wp)
|
||||||
|
|
||||||
|
|
||||||
class StrictWeaknessTests(MM2TestBase):
|
class WeaknessTests(MM2TestBase):
|
||||||
options = {
|
options = {
|
||||||
"strict_weakness": True,
|
|
||||||
"yoku_jumps": True,
|
"yoku_jumps": True,
|
||||||
"enable_lasers": True
|
"enable_lasers": True,
|
||||||
}
|
}
|
||||||
|
|
||||||
def test_that_every_boss_has_a_weakness(self) -> None:
|
def test_that_every_boss_has_a_weakness(self) -> None:
|
||||||
world = self.multiworld.worlds[self.player]
|
world = self.multiworld.worlds[self.player]
|
||||||
weapon_damage = world.weapon_damage
|
weapon_damage = world.weapon_damage
|
||||||
for boss in range(14):
|
for boss in range(14):
|
||||||
if not any(weapon_damage[weapon][boss] for weapon in range(9)):
|
if not any(weapon_damage[weapon][boss] >= minimum_weakness_requirement[weapon] for weapon in range(9)):
|
||||||
self.fail(f"Boss {boss} generated without weakness! Seed: {self.multiworld.seed}")
|
self.fail(f"Boss {boss} generated without weakness! Seed: {self.multiworld.seed}")
|
||||||
|
|
||||||
def test_wily_5(self) -> None:
|
def test_wily_5(self) -> None:
|
||||||
validate_wily_5(self)
|
validate_wily_5(self)
|
||||||
|
|
||||||
|
|
||||||
class RandomStrictWeaknessTests(MM2TestBase):
|
class StrictWeaknessTests(WeaknessTests):
|
||||||
|
options = {
|
||||||
|
"strict_weakness": True,
|
||||||
|
**WeaknessTests.options
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class RandomWeaknessTests(WeaknessTests):
|
||||||
|
options = {
|
||||||
|
"random_weakness": "randomized",
|
||||||
|
**WeaknessTests.options
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class ShuffledWeaknessTests(WeaknessTests):
|
||||||
|
options = {
|
||||||
|
"random_weakness": "shuffled",
|
||||||
|
**WeaknessTests.options
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
class RandomStrictWeaknessTests(WeaknessTests):
|
||||||
options = {
|
options = {
|
||||||
"strict_weakness": True,
|
"strict_weakness": True,
|
||||||
"random_weakness": "randomized",
|
"random_weakness": "randomized",
|
||||||
"yoku_jumps": True,
|
**WeaknessTests.options
|
||||||
"enable_lasers": True
|
|
||||||
}
|
}
|
||||||
|
|
||||||
def test_that_every_boss_has_a_weakness(self) -> None:
|
|
||||||
world = self.multiworld.worlds[self.player]
|
|
||||||
weapon_damage = world.weapon_damage
|
|
||||||
for boss in range(14):
|
|
||||||
if not any(weapon_damage[weapon][boss] for weapon in range(9)):
|
|
||||||
self.fail(f"Boss {boss} generated without weakness! Seed: {self.multiworld.seed}")
|
|
||||||
|
|
||||||
def test_wily_5(self) -> None:
|
class ShuffledStrictWeaknessTests(WeaknessTests):
|
||||||
validate_wily_5(self)
|
options = {
|
||||||
|
"strict_weakness": True,
|
||||||
|
"random_weakness": "shuffled",
|
||||||
|
**WeaknessTests.options
|
||||||
|
}
|
||||||
|
|||||||
@@ -62,7 +62,7 @@ chunksanity_starting_chunks: typing.List[str] = [
|
|||||||
ItemNames.South_Of_Varrock,
|
ItemNames.South_Of_Varrock,
|
||||||
ItemNames.Central_Varrock,
|
ItemNames.Central_Varrock,
|
||||||
ItemNames.Varrock_Palace,
|
ItemNames.Varrock_Palace,
|
||||||
ItemNames.East_Of_Varrock,
|
ItemNames.Lumberyard,
|
||||||
ItemNames.West_Varrock,
|
ItemNames.West_Varrock,
|
||||||
ItemNames.Edgeville,
|
ItemNames.Edgeville,
|
||||||
ItemNames.Barbarian_Village,
|
ItemNames.Barbarian_Village,
|
||||||
|
|||||||
@@ -8,7 +8,9 @@ import requests
|
|||||||
# The CSVs are updated at this repository to be shared between generator and client.
|
# The CSVs are updated at this repository to be shared between generator and client.
|
||||||
data_repository_address = "https://raw.githubusercontent.com/digiholic/osrs-archipelago-logic/"
|
data_repository_address = "https://raw.githubusercontent.com/digiholic/osrs-archipelago-logic/"
|
||||||
# The Github tag of the CSVs this was generated with
|
# The Github tag of the CSVs this was generated with
|
||||||
data_csv_tag = "v1.5"
|
data_csv_tag = "v2.0.4"
|
||||||
|
# If true, generate using file names in the repository
|
||||||
|
debug = False
|
||||||
|
|
||||||
if __name__ == "__main__":
|
if __name__ == "__main__":
|
||||||
import sys
|
import sys
|
||||||
@@ -26,98 +28,167 @@ if __name__ == "__main__":
|
|||||||
def load_location_csv():
|
def load_location_csv():
|
||||||
this_dir = os.path.dirname(os.path.abspath(__file__))
|
this_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
with open(os.path.join(this_dir, "locations_generated.py"), 'w+') as locPyFile:
|
with open(os.path.join(this_dir, "locations_generated.py"), 'w+') as loc_py_file:
|
||||||
locPyFile.write('"""\nThis file was auto generated by LogicCSVToPython.py\n"""\n')
|
loc_py_file.write('"""\nThis file was auto generated by LogicCSVToPython.py\n"""\n')
|
||||||
locPyFile.write("from ..Locations import LocationRow, SkillRequirement\n")
|
loc_py_file.write("from ..Locations import LocationRow, SkillRequirement\n")
|
||||||
locPyFile.write("\n")
|
loc_py_file.write("\n")
|
||||||
locPyFile.write("location_rows = [\n")
|
loc_py_file.write("location_rows = [\n")
|
||||||
|
|
||||||
with requests.get(data_repository_address + "/" + data_csv_tag + "/locations.csv") as req:
|
if debug:
|
||||||
locations_reader = csv.reader(req.text.splitlines())
|
with open(os.path.join(this_dir, "locations.csv"), "r") as loc_file:
|
||||||
for row in locations_reader:
|
locations_reader = csv.reader(loc_file.read().splitlines())
|
||||||
row_line = "LocationRow("
|
parse_loc_file(loc_py_file, locations_reader)
|
||||||
row_line += str_format(row[0])
|
else:
|
||||||
row_line += str_format(row[1].lower())
|
print("Loading: " + data_repository_address + "/" + data_csv_tag + "/locations.csv")
|
||||||
|
with requests.get(data_repository_address + "/" + data_csv_tag + "/locations.csv") as req:
|
||||||
|
if req.status_code == 200:
|
||||||
|
locations_reader = csv.reader(req.text.splitlines())
|
||||||
|
parse_loc_file(loc_py_file, locations_reader)
|
||||||
|
else:
|
||||||
|
print(str(req.status_code) + ": " + req.reason)
|
||||||
|
loc_py_file.write("]\n")
|
||||||
|
|
||||||
region_strings = row[2].split(", ") if row[2] else []
|
|
||||||
row_line += f"{str_list_to_py(region_strings)}, "
|
|
||||||
|
|
||||||
skill_strings = row[3].split(", ")
|
def parse_loc_file(loc_py_file, locations_reader):
|
||||||
row_line += "["
|
for row in locations_reader:
|
||||||
if skill_strings:
|
# Skip the header row, if present
|
||||||
split_skills = [skill.split(" ") for skill in skill_strings if skill != ""]
|
if row[0] == "Location Name":
|
||||||
if split_skills:
|
continue
|
||||||
for split in split_skills:
|
row_line = "LocationRow("
|
||||||
row_line += f"SkillRequirement('{split[0]}', {split[1]}), "
|
row_line += str_format(row[0])
|
||||||
row_line += "], "
|
row_line += str_format(row[1].lower())
|
||||||
|
|
||||||
|
region_strings = row[2].split(", ") if row[2] else []
|
||||||
|
row_line += f"{str_list_to_py(region_strings)}, "
|
||||||
|
|
||||||
|
skill_strings = row[3].split(", ")
|
||||||
|
row_line += "["
|
||||||
|
if skill_strings:
|
||||||
|
split_skills = [skill.split(" ") for skill in skill_strings if skill != ""]
|
||||||
|
if split_skills:
|
||||||
|
for split in split_skills:
|
||||||
|
row_line += f"SkillRequirement('{split[0]}', {split[1]}), "
|
||||||
|
row_line += "], "
|
||||||
|
|
||||||
|
item_strings = row[4].split(", ") if row[4] else []
|
||||||
|
row_line += f"{str_list_to_py(item_strings)}, "
|
||||||
|
row_line += f"{row[5]})" if row[5] != "" else "0)"
|
||||||
|
loc_py_file.write(f"\t{row_line},\n")
|
||||||
|
|
||||||
item_strings = row[4].split(", ") if row[4] else []
|
|
||||||
row_line += f"{str_list_to_py(item_strings)}, "
|
|
||||||
row_line += f"{row[5]})" if row[5] != "" else "0)"
|
|
||||||
locPyFile.write(f"\t{row_line},\n")
|
|
||||||
locPyFile.write("]\n")
|
|
||||||
|
|
||||||
def load_region_csv():
|
def load_region_csv():
|
||||||
this_dir = os.path.dirname(os.path.abspath(__file__))
|
this_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
with open(os.path.join(this_dir, "regions_generated.py"), 'w+') as regPyFile:
|
with open(os.path.join(this_dir, "regions_generated.py"), 'w+') as reg_py_file:
|
||||||
regPyFile.write('"""\nThis file was auto generated by LogicCSVToPython.py\n"""\n')
|
reg_py_file.write('"""\nThis file was auto generated by LogicCSVToPython.py\n"""\n')
|
||||||
regPyFile.write("from ..Regions import RegionRow\n")
|
reg_py_file.write("from ..Regions import RegionRow\n")
|
||||||
regPyFile.write("\n")
|
reg_py_file.write("\n")
|
||||||
regPyFile.write("region_rows = [\n")
|
reg_py_file.write("region_rows = [\n")
|
||||||
|
|
||||||
|
if debug:
|
||||||
|
with open(os.path.join(this_dir, "regions.csv"), "r") as region_file:
|
||||||
|
regions_reader = csv.reader(region_file.read().splitlines())
|
||||||
|
parse_region_file(reg_py_file, regions_reader)
|
||||||
|
else:
|
||||||
|
print("Loading: "+ data_repository_address + "/" + data_csv_tag + "/regions.csv")
|
||||||
|
with requests.get(data_repository_address + "/" + data_csv_tag + "/regions.csv") as req:
|
||||||
|
if req.status_code == 200:
|
||||||
|
regions_reader = csv.reader(req.text.splitlines())
|
||||||
|
parse_region_file(reg_py_file, regions_reader)
|
||||||
|
else:
|
||||||
|
print(str(req.status_code) + ": " + req.reason)
|
||||||
|
reg_py_file.write("]\n")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_region_file(reg_py_file, regions_reader):
|
||||||
|
for row in regions_reader:
|
||||||
|
# Skip the header row, if present
|
||||||
|
if row[0] == "Region Name":
|
||||||
|
continue
|
||||||
|
|
||||||
|
row_line = "RegionRow("
|
||||||
|
row_line += str_format(row[0])
|
||||||
|
row_line += str_format(row[1])
|
||||||
|
connections = row[2]
|
||||||
|
row_line += f"{str_list_to_py(connections.split(', '))}, "
|
||||||
|
resources = row[3]
|
||||||
|
row_line += f"{str_list_to_py(resources.split(', '))})"
|
||||||
|
reg_py_file.write(f"\t{row_line},\n")
|
||||||
|
|
||||||
with requests.get(data_repository_address + "/" + data_csv_tag + "/regions.csv") as req:
|
|
||||||
regions_reader = csv.reader(req.text.splitlines())
|
|
||||||
for row in regions_reader:
|
|
||||||
row_line = "RegionRow("
|
|
||||||
row_line += str_format(row[0])
|
|
||||||
row_line += str_format(row[1])
|
|
||||||
connections = row[2].replace("'", "\\'")
|
|
||||||
row_line += f"{str_list_to_py(connections.split(', '))}, "
|
|
||||||
resources = row[3].replace("'", "\\'")
|
|
||||||
row_line += f"{str_list_to_py(resources.split(', '))})"
|
|
||||||
regPyFile.write(f"\t{row_line},\n")
|
|
||||||
regPyFile.write("]\n")
|
|
||||||
|
|
||||||
def load_resource_csv():
|
def load_resource_csv():
|
||||||
this_dir = os.path.dirname(os.path.abspath(__file__))
|
this_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
with open(os.path.join(this_dir, "resources_generated.py"), 'w+') as resPyFile:
|
with open(os.path.join(this_dir, "resources_generated.py"), 'w+') as res_py_file:
|
||||||
resPyFile.write('"""\nThis file was auto generated by LogicCSVToPython.py\n"""\n')
|
res_py_file.write('"""\nThis file was auto generated by LogicCSVToPython.py\n"""\n')
|
||||||
resPyFile.write("from ..Regions import ResourceRow\n")
|
res_py_file.write("from ..Regions import ResourceRow\n")
|
||||||
resPyFile.write("\n")
|
res_py_file.write("\n")
|
||||||
resPyFile.write("resource_rows = [\n")
|
res_py_file.write("resource_rows = [\n")
|
||||||
|
|
||||||
with requests.get(data_repository_address + "/" + data_csv_tag + "/resources.csv") as req:
|
if debug:
|
||||||
resource_reader = csv.reader(req.text.splitlines())
|
with open(os.path.join(this_dir, "resources.csv"), "r") as region_file:
|
||||||
for row in resource_reader:
|
regions_reader = csv.reader(region_file.read().splitlines())
|
||||||
name = row[0].replace("'", "\\'")
|
parse_resources_file(res_py_file, regions_reader)
|
||||||
row_line = f"ResourceRow('{name}')"
|
else:
|
||||||
resPyFile.write(f"\t{row_line},\n")
|
print("Loading: " + data_repository_address + "/" + data_csv_tag + "/resources.csv")
|
||||||
resPyFile.write("]\n")
|
with requests.get(data_repository_address + "/" + data_csv_tag + "/resources.csv") as req:
|
||||||
|
if req.status_code == 200:
|
||||||
|
resource_reader = csv.reader(req.text.splitlines())
|
||||||
|
parse_resources_file(res_py_file, resource_reader)
|
||||||
|
else:
|
||||||
|
print(str(req.status_code) + ": " + req.reason)
|
||||||
|
res_py_file.write("]\n")
|
||||||
|
|
||||||
|
|
||||||
|
def parse_resources_file(res_py_file, resource_reader):
|
||||||
|
for row in resource_reader:
|
||||||
|
# Skip the header row, if present
|
||||||
|
if row[0] == "Resource Name":
|
||||||
|
continue
|
||||||
|
|
||||||
|
name = row[0].replace("'", "\\'")
|
||||||
|
row_line = f"ResourceRow('{name}')"
|
||||||
|
res_py_file.write(f"\t{row_line},\n")
|
||||||
|
|
||||||
|
|
||||||
def load_item_csv():
|
def load_item_csv():
|
||||||
this_dir = os.path.dirname(os.path.abspath(__file__))
|
this_dir = os.path.dirname(os.path.abspath(__file__))
|
||||||
|
|
||||||
with open(os.path.join(this_dir, "items_generated.py"), 'w+') as itemPyfile:
|
with open(os.path.join(this_dir, "items_generated.py"), 'w+') as item_py_file:
|
||||||
itemPyfile.write('"""\nThis file was auto generated by LogicCSVToPython.py\n"""\n')
|
item_py_file.write('"""\nThis file was auto generated by LogicCSVToPython.py\n"""\n')
|
||||||
itemPyfile.write("from BaseClasses import ItemClassification\n")
|
item_py_file.write("from BaseClasses import ItemClassification\n")
|
||||||
itemPyfile.write("from ..Items import ItemRow\n")
|
item_py_file.write("from ..Items import ItemRow\n")
|
||||||
itemPyfile.write("\n")
|
item_py_file.write("\n")
|
||||||
itemPyfile.write("item_rows = [\n")
|
item_py_file.write("item_rows = [\n")
|
||||||
|
|
||||||
with requests.get(data_repository_address + "/" + data_csv_tag + "/items.csv") as req:
|
if debug:
|
||||||
item_reader = csv.reader(req.text.splitlines())
|
with open(os.path.join(this_dir, "items.csv"), "r") as region_file:
|
||||||
for row in item_reader:
|
regions_reader = csv.reader(region_file.read().splitlines())
|
||||||
row_line = "ItemRow("
|
parse_item_file(item_py_file, regions_reader)
|
||||||
row_line += str_format(row[0])
|
else:
|
||||||
row_line += f"{row[1]}, "
|
print("Loading: " + data_repository_address + "/" + data_csv_tag + "/items.csv")
|
||||||
|
with requests.get(data_repository_address + "/" + data_csv_tag + "/items.csv") as req:
|
||||||
|
if req.status_code == 200:
|
||||||
|
item_reader = csv.reader(req.text.splitlines())
|
||||||
|
parse_item_file(item_py_file, item_reader)
|
||||||
|
else:
|
||||||
|
print(str(req.status_code) + ": " + req.reason)
|
||||||
|
item_py_file.write("]\n")
|
||||||
|
|
||||||
row_line += f"ItemClassification.{row[2]})"
|
|
||||||
|
|
||||||
itemPyfile.write(f"\t{row_line},\n")
|
def parse_item_file(item_py_file, item_reader):
|
||||||
itemPyfile.write("]\n")
|
for row in item_reader:
|
||||||
|
# Skip the header row, if present
|
||||||
|
if row[0] == "Name":
|
||||||
|
continue
|
||||||
|
|
||||||
|
row_line = "ItemRow("
|
||||||
|
row_line += str_format(row[0])
|
||||||
|
row_line += f"{row[1]}, "
|
||||||
|
|
||||||
|
row_line += f"ItemClassification.{row[2]})"
|
||||||
|
|
||||||
|
item_py_file.write(f"\t{row_line},\n")
|
||||||
|
|
||||||
|
|
||||||
def str_format(s) -> str:
|
def str_format(s) -> str:
|
||||||
@@ -128,7 +199,7 @@ if __name__ == "__main__":
|
|||||||
def str_list_to_py(str_list) -> str:
|
def str_list_to_py(str_list) -> str:
|
||||||
ret_str = "["
|
ret_str = "["
|
||||||
for s in str_list:
|
for s in str_list:
|
||||||
ret_str += f"'{s}', "
|
ret_str += str_format(s)
|
||||||
ret_str += "]"
|
ret_str += "]"
|
||||||
return ret_str
|
return ret_str
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,7 @@ item_rows = [
|
|||||||
ItemRow('Area: HAM Hideout', 1, ItemClassification.progression),
|
ItemRow('Area: HAM Hideout', 1, ItemClassification.progression),
|
||||||
ItemRow('Area: Lumbridge Farms', 1, ItemClassification.progression),
|
ItemRow('Area: Lumbridge Farms', 1, ItemClassification.progression),
|
||||||
ItemRow('Area: South of Varrock', 1, ItemClassification.progression),
|
ItemRow('Area: South of Varrock', 1, ItemClassification.progression),
|
||||||
ItemRow('Area: East Varrock', 1, ItemClassification.progression),
|
ItemRow('Area: Lumberyard', 1, ItemClassification.progression),
|
||||||
ItemRow('Area: Central Varrock', 1, ItemClassification.progression),
|
ItemRow('Area: Central Varrock', 1, ItemClassification.progression),
|
||||||
ItemRow('Area: Varrock Palace', 1, ItemClassification.progression),
|
ItemRow('Area: Varrock Palace', 1, ItemClassification.progression),
|
||||||
ItemRow('Area: West Varrock', 1, ItemClassification.progression),
|
ItemRow('Area: West Varrock', 1, ItemClassification.progression),
|
||||||
@@ -37,7 +37,58 @@ item_rows = [
|
|||||||
ItemRow('Progressive Armor', 6, ItemClassification.progression),
|
ItemRow('Progressive Armor', 6, ItemClassification.progression),
|
||||||
ItemRow('Progressive Weapons', 6, ItemClassification.progression),
|
ItemRow('Progressive Weapons', 6, ItemClassification.progression),
|
||||||
ItemRow('Progressive Tools', 6, ItemClassification.useful),
|
ItemRow('Progressive Tools', 6, ItemClassification.useful),
|
||||||
ItemRow('Progressive Ranged Weapons', 3, ItemClassification.useful),
|
ItemRow('Progressive Ranged Weapon', 3, ItemClassification.useful),
|
||||||
ItemRow('Progressive Ranged Armor', 3, ItemClassification.useful),
|
ItemRow('Progressive Ranged Armor', 3, ItemClassification.useful),
|
||||||
ItemRow('Progressive Magic', 2, ItemClassification.useful),
|
ItemRow('Progressive Magic Spell', 2, ItemClassification.useful),
|
||||||
|
ItemRow('An Invitation to the Gielinor Games', 1, ItemClassification.filler),
|
||||||
|
ItemRow('Settled\'s Crossbow', 1, ItemClassification.filler),
|
||||||
|
ItemRow('The Stone of Jas', 1, ItemClassification.filler),
|
||||||
|
ItemRow('Nieve\'s Phone Number', 1, ItemClassification.filler),
|
||||||
|
ItemRow('Hannanie\'s Lost Sanity', 1, ItemClassification.filler),
|
||||||
|
ItemRow('XP Waste', 1, ItemClassification.filler),
|
||||||
|
ItemRow('Ten Free Pulls on the Squeal of Fortune', 1, ItemClassification.filler),
|
||||||
|
ItemRow('Project Zanaris Beta Invite', 1, ItemClassification.filler),
|
||||||
|
ItemRow('A Funny Feeling You Would Have Been Followed', 1, ItemClassification.filler),
|
||||||
|
ItemRow('An Ominous Prediction From Gnome Child', 1, ItemClassification.filler),
|
||||||
|
ItemRow('A Logic Error', 1, ItemClassification.filler),
|
||||||
|
ItemRow('The Warding Skill', 1, ItemClassification.filler),
|
||||||
|
ItemRow('A 1/2500 Chance At Your Very Own Pet Baron Sucellus, Redeemable at your Local Duke, Some Restrictions May Apply', 1, ItemClassification.filler),
|
||||||
|
ItemRow('A Suspicious Email From Iagex.com Asking for your Password', 1, ItemClassification.filler),
|
||||||
|
ItemRow('A Review on that Pull Request You\'ve Been Waiting On', 1, ItemClassification.filler),
|
||||||
|
ItemRow('Fifty Billion RS3 GP (Worthless)', 1, ItemClassification.filler),
|
||||||
|
ItemRow('Mod Ash\'s Coffee Cup', 1, ItemClassification.filler),
|
||||||
|
ItemRow('An Embarrasing Photo of Zammorak at the Christmas Party', 1, ItemClassification.filler),
|
||||||
|
ItemRow('Another Bug To Report', 1, ItemClassification.filler),
|
||||||
|
ItemRow('1-Up Mushroom', 1, ItemClassification.filler),
|
||||||
|
ItemRow('Empty White Hallways', 1, ItemClassification.filler),
|
||||||
|
ItemRow('Area: Menaphos', 1, ItemClassification.filler),
|
||||||
|
ItemRow('A Ratcatchers Dialogue Rewrite', 1, ItemClassification.filler),
|
||||||
|
ItemRow('"Nostalgia"', 1, ItemClassification.filler),
|
||||||
|
ItemRow('A Hornless Unicorn', 1, ItemClassification.filler),
|
||||||
|
ItemRow('The Ability To Use ::bank', 1, ItemClassification.filler),
|
||||||
|
ItemRow('Free Haircut at the Falador Hairdresser', 1, ItemClassification.filler),
|
||||||
|
ItemRow('Nothing Interesting Happens', 1, ItemClassification.filler),
|
||||||
|
ItemRow('Why Fletch?', 1, ItemClassification.filler),
|
||||||
|
ItemRow('Evolution of Combat', 1, ItemClassification.filler),
|
||||||
|
ItemRow('Care Pack: 10,000 GP', 1, ItemClassification.useful),
|
||||||
|
ItemRow('Care Pack: 90 Steel Nails', 1, ItemClassification.useful),
|
||||||
|
ItemRow('Care Pack: 25 Swordfish', 1, ItemClassification.useful),
|
||||||
|
ItemRow('Care Pack: 50 Lobsters', 1, ItemClassification.useful),
|
||||||
|
ItemRow('Care Pack: 100 Law Runes', 1, ItemClassification.useful),
|
||||||
|
ItemRow('Care Pack: 300 Each Elemental Rune', 1, ItemClassification.useful),
|
||||||
|
ItemRow('Care Pack: 100 Chaos Runes', 1, ItemClassification.useful),
|
||||||
|
ItemRow('Care Pack: 100 Death Runes', 1, ItemClassification.useful),
|
||||||
|
ItemRow('Care Pack: 100 Oak Logs', 1, ItemClassification.useful),
|
||||||
|
ItemRow('Care Pack: 50 Willow Logs', 1, ItemClassification.useful),
|
||||||
|
ItemRow('Care Pack: 50 Bronze Bars', 1, ItemClassification.useful),
|
||||||
|
ItemRow('Care Pack: 200 Iron Ore', 1, ItemClassification.useful),
|
||||||
|
ItemRow('Care Pack: 100 Coal Ore', 1, ItemClassification.useful),
|
||||||
|
ItemRow('Care Pack: 100 Raw Trout', 1, ItemClassification.useful),
|
||||||
|
ItemRow('Care Pack: 200 Leather', 1, ItemClassification.useful),
|
||||||
|
ItemRow('Care Pack: 50 Energy Potion (4)', 2, ItemClassification.useful),
|
||||||
|
ItemRow('Care Pack: 200 Big Bones', 1, ItemClassification.useful),
|
||||||
|
ItemRow('Care Pack: 10 Each Uncut gems', 1, ItemClassification.useful),
|
||||||
|
ItemRow('Care Pack: 3 Rings of Forging', 1, ItemClassification.useful),
|
||||||
|
ItemRow('Care Pack: 500 Rune Essence', 1, ItemClassification.useful),
|
||||||
|
ItemRow('Care Pack: 200 Mind Runes', 1, ItemClassification.useful),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -19,37 +19,56 @@ location_rows = [
|
|||||||
LocationRow('Quest: Witch\'s Potion', 'quest', ['Rimmington', 'Port Sarim', ], [], [], 0),
|
LocationRow('Quest: Witch\'s Potion', 'quest', ['Rimmington', 'Port Sarim', ], [], [], 0),
|
||||||
LocationRow('Quest: The Knight\'s Sword', 'quest', ['Falador', 'Varrock Palace', 'Mudskipper Point', 'South of Varrock', 'Windmill', 'Pie Dish', 'Port Sarim', ], [SkillRequirement('Cooking', 10), SkillRequirement('Mining', 10), ], [], 0),
|
LocationRow('Quest: The Knight\'s Sword', 'quest', ['Falador', 'Varrock Palace', 'Mudskipper Point', 'South of Varrock', 'Windmill', 'Pie Dish', 'Port Sarim', ], [SkillRequirement('Cooking', 10), SkillRequirement('Mining', 10), ], [], 0),
|
||||||
LocationRow('Quest: Goblin Diplomacy', 'quest', ['Goblin Village', 'Draynor Village', 'Falador', 'South of Varrock', 'Onion', ], [], [], 0),
|
LocationRow('Quest: Goblin Diplomacy', 'quest', ['Goblin Village', 'Draynor Village', 'Falador', 'South of Varrock', 'Onion', ], [], [], 0),
|
||||||
LocationRow('Quest: Pirate\'s Treasure', 'quest', ['Port Sarim', 'Karamja', 'Falador', ], [], [], 0),
|
LocationRow('Quest: Pirate\'s Treasure', 'quest', ['Port Sarim', 'Karamja', 'Falador', 'Central Varrock', ], [], [], 0),
|
||||||
LocationRow('Quest: Rune Mysteries', 'quest', ['Lumbridge', 'Wizard Tower', 'Central Varrock', ], [], [], 0),
|
LocationRow('Quest: Rune Mysteries', 'quest', ['Lumbridge', 'Wizard Tower', 'Central Varrock', ], [], [], 0),
|
||||||
LocationRow('Quest: Misthalin Mystery', 'quest', ['Lumbridge Swamp', ], [], [], 0),
|
LocationRow('Quest: Misthalin Mystery', 'quest', ['Lumbridge Swamp', ], [], [], 0),
|
||||||
LocationRow('Quest: The Corsair Curse', 'quest', ['Rimmington', 'Falador Farms', 'Corsair Cove', ], [], [], 0),
|
LocationRow('Quest: The Corsair Curse', 'quest', ['Rimmington', 'Falador Farms', 'Corsair Cove', ], [], [], 0),
|
||||||
LocationRow('Quest: X Marks the Spot', 'quest', ['Lumbridge', 'Draynor Village', 'Port Sarim', ], [], [], 0),
|
LocationRow('Quest: X Marks the Spot', 'quest', ['Lumbridge', 'Draynor Village', 'Port Sarim', ], [], [], 0),
|
||||||
LocationRow('Quest: Below Ice Mountain', 'quest', ['Dwarven Mines', 'Dwarven Mountain Pass', 'Ice Mountain', 'Barbarian Village', 'Falador', 'Central Varrock', 'Edgeville', ], [], [], 16),
|
LocationRow('Quest: Below Ice Mountain', 'quest', ['Dwarven Mines', 'Dwarven Mountain Pass', 'Ice Mountain', 'Barbarian Village', 'Falador', 'Central Varrock', 'Edgeville', ], [], [], 16),
|
||||||
LocationRow('Quest: Dragon Slayer', 'goal', ['Crandor', 'South of Varrock', 'Edgeville', 'Lumbridge', 'Rimmington', 'Monastery', 'Dwarven Mines', 'Port Sarim', 'Draynor Village', ], [], [], 32),
|
LocationRow('Quest: Dragon Slayer', 'goal', ['Crandor', 'South of Varrock', 'Edgeville', 'Lumbridge', 'Rimmington', 'Monastery', 'Dwarven Mines', 'Port Sarim', 'Draynor Village', ], [], [], 32),
|
||||||
|
LocationRow('Bury Some Big Bones', 'prayer', ['Big Bones', ], [SkillRequirement('Prayer', 1), ], [], 0),
|
||||||
|
LocationRow('Activate the "Sharp Eye" Prayer', 'prayer', [], [SkillRequirement('Prayer', 8), ], [], 0),
|
||||||
LocationRow('Activate the "Rock Skin" Prayer', 'prayer', [], [SkillRequirement('Prayer', 10), ], [], 0),
|
LocationRow('Activate the "Rock Skin" Prayer', 'prayer', [], [SkillRequirement('Prayer', 10), ], [], 0),
|
||||||
LocationRow('Activate the "Protect Item" Prayer', 'prayer', [], [SkillRequirement('Prayer', 25), ], [], 2),
|
LocationRow('Activate the "Protect Item" Prayer', 'prayer', [], [SkillRequirement('Prayer', 25), ], [], 2),
|
||||||
LocationRow('Pray at the Edgeville Monastery', 'prayer', ['Monastery', ], [SkillRequirement('Prayer', 31), ], [], 6),
|
LocationRow('Pray at the Edgeville Monastery', 'prayer', ['Monastery', ], [SkillRequirement('Prayer', 31), ], [], 6),
|
||||||
LocationRow('Cast Bones To Bananas', 'magic', ['Nature Runes', ], [SkillRequirement('Magic', 15), ], [], 0),
|
LocationRow('Cast Bones To Bananas', 'magic', ['Nature Runes', ], [SkillRequirement('Magic', 15), ], [], 0),
|
||||||
|
LocationRow('Cast Earth Strike', 'magic', [], [SkillRequirement('Magic', 9), ], [], 0),
|
||||||
|
LocationRow('Cast Curse', 'magic', [], [SkillRequirement('Magic', 19), ], [], 0),
|
||||||
LocationRow('Teleport to Varrock', 'magic', ['Central Varrock', 'Law Runes', ], [SkillRequirement('Magic', 25), ], [], 0),
|
LocationRow('Teleport to Varrock', 'magic', ['Central Varrock', 'Law Runes', ], [SkillRequirement('Magic', 25), ], [], 0),
|
||||||
LocationRow('Teleport to Lumbridge', 'magic', ['Lumbridge', 'Law Runes', ], [SkillRequirement('Magic', 31), ], [], 2),
|
LocationRow('Teleport to Lumbridge', 'magic', ['Lumbridge', 'Law Runes', ], [SkillRequirement('Magic', 31), ], [], 0),
|
||||||
|
LocationRow('Telegrab a Gold Bar from the Varrock Bank', 'magic', ['Law Runes', 'West Varrock', ], [SkillRequirement('Magic', 33), ], [], 0),
|
||||||
LocationRow('Teleport to Falador', 'magic', ['Falador', 'Law Runes', ], [SkillRequirement('Magic', 37), ], [], 6),
|
LocationRow('Teleport to Falador', 'magic', ['Falador', 'Law Runes', ], [SkillRequirement('Magic', 37), ], [], 6),
|
||||||
LocationRow('Craft an Air Rune', 'runecraft', ['Rune Essence', 'Falador Farms', ], [SkillRequirement('Runecraft', 1), ], [], 0),
|
LocationRow('Craft an Air Rune', 'runecraft', ['Rune Essence', 'Falador Farms', ], [SkillRequirement('Runecraft', 1), ], [], 0),
|
||||||
|
LocationRow('Craft a Mind Rune', 'runecraft', ['Rune Essence', 'Goblin Village', ], [SkillRequirement('Runecraft', 2), ], [], 0),
|
||||||
|
LocationRow('Craft a Water Rune', 'runecraft', ['Rune Essence', 'Lumbridge Swamp', ], [SkillRequirement('Runecraft', 5), ], [], 0),
|
||||||
|
LocationRow('Craft an Earth Rune', 'runecraft', ['Rune Essence', 'Lumberyard', ], [SkillRequirement('Runecraft', 9), ], [], 0),
|
||||||
|
LocationRow('Craft a Fire Rune', 'runecraft', ['Rune Essence', 'Al Kharid', ], [SkillRequirement('Runecraft', 14), ], [], 0),
|
||||||
|
LocationRow('Craft a Body Rune', 'runecraft', ['Rune Essence', 'Dwarven Mountain Pass', ], [SkillRequirement('Runecraft', 20), ], [], 0),
|
||||||
LocationRow('Craft runes with a Mind Core', 'runecraft', ['Camdozaal', 'Goblin Village', ], [SkillRequirement('Runecraft', 2), ], [], 0),
|
LocationRow('Craft runes with a Mind Core', 'runecraft', ['Camdozaal', 'Goblin Village', ], [SkillRequirement('Runecraft', 2), ], [], 0),
|
||||||
LocationRow('Craft runes with a Body Core', 'runecraft', ['Camdozaal', 'Dwarven Mountain Pass', ], [SkillRequirement('Runecraft', 20), ], [], 0),
|
LocationRow('Craft runes with a Body Core', 'runecraft', ['Camdozaal', 'Dwarven Mountain Pass', ], [SkillRequirement('Runecraft', 20), ], [], 0),
|
||||||
|
LocationRow('Craft a Pot', 'crafting', ['Clay Ore', 'Barbarian Village', ], [SkillRequirement('Crafting', 1), ], [], 0),
|
||||||
|
LocationRow('Craft a pair of Leather Boots', 'crafting', ['Milk', 'Al Kharid', ], [SkillRequirement('Crafting', 7), ], [], 0),
|
||||||
LocationRow('Make an Unblessed Symbol', 'crafting', ['Silver Ore', 'Furnace', 'Al Kharid', 'Sheep', 'Spinning Wheel', ], [SkillRequirement('Crafting', 16), ], [], 0),
|
LocationRow('Make an Unblessed Symbol', 'crafting', ['Silver Ore', 'Furnace', 'Al Kharid', 'Sheep', 'Spinning Wheel', ], [SkillRequirement('Crafting', 16), ], [], 0),
|
||||||
LocationRow('Cut a Sapphire', 'crafting', ['Chisel', ], [SkillRequirement('Crafting', 20), ], [], 0),
|
LocationRow('Cut a Sapphire', 'crafting', ['Chisel', ], [SkillRequirement('Crafting', 20), ], [], 0),
|
||||||
LocationRow('Cut an Emerald', 'crafting', ['Chisel', ], [SkillRequirement('Crafting', 27), ], [], 0),
|
LocationRow('Cut an Emerald', 'crafting', ['Chisel', ], [SkillRequirement('Crafting', 27), ], [], 0),
|
||||||
LocationRow('Cut a Ruby', 'crafting', ['Chisel', ], [SkillRequirement('Crafting', 34), ], [], 4),
|
LocationRow('Cut a Ruby', 'crafting', ['Chisel', ], [SkillRequirement('Crafting', 34), ], [], 4),
|
||||||
|
LocationRow('Enter the Crafting Guild', 'crafting', ['Crafting Guild', ], [SkillRequirement('Crafting', 40), ], [], 0),
|
||||||
LocationRow('Cut a Diamond', 'crafting', ['Chisel', ], [SkillRequirement('Crafting', 43), ], [], 8),
|
LocationRow('Cut a Diamond', 'crafting', ['Chisel', ], [SkillRequirement('Crafting', 43), ], [], 8),
|
||||||
|
LocationRow('Mine Copper', 'crafting', ['Bronze Ores', ], [SkillRequirement('Mining', 1), ], [], 0),
|
||||||
|
LocationRow('Mine Tin', 'crafting', ['Bronze Ores', ], [SkillRequirement('Mining', 1), ], [], 0),
|
||||||
|
LocationRow('Mine Clay', 'crafting', ['Clay Ore', ], [SkillRequirement('Mining', 1), ], [], 0),
|
||||||
|
LocationRow('Mine Iron', 'mining', ['Iron Ore', ], [SkillRequirement('Mining', 1), ], [], 0),
|
||||||
LocationRow('Mine a Blurite Ore', 'mining', ['Mudskipper Point', 'Port Sarim', ], [SkillRequirement('Mining', 10), ], [], 0),
|
LocationRow('Mine a Blurite Ore', 'mining', ['Mudskipper Point', 'Port Sarim', ], [SkillRequirement('Mining', 10), ], [], 0),
|
||||||
LocationRow('Crush a Barronite Deposit', 'mining', ['Camdozaal', ], [SkillRequirement('Mining', 14), ], [], 0),
|
LocationRow('Crush a Barronite Deposit', 'mining', ['Camdozaal', ], [SkillRequirement('Mining', 14), ], [], 0),
|
||||||
LocationRow('Mine Silver', 'mining', ['Silver Ore', ], [SkillRequirement('Mining', 20), ], [], 0),
|
LocationRow('Mine Silver', 'mining', ['Silver Ore', ], [SkillRequirement('Mining', 20), ], [], 0),
|
||||||
LocationRow('Mine Coal', 'mining', ['Coal Ore', ], [SkillRequirement('Mining', 30), ], [], 2),
|
LocationRow('Mine Coal', 'mining', ['Coal Ore', ], [SkillRequirement('Mining', 30), ], [], 2),
|
||||||
LocationRow('Mine Gold', 'mining', ['Gold Ore', ], [SkillRequirement('Mining', 40), ], [], 6),
|
LocationRow('Mine Gold', 'mining', ['Gold Ore', ], [SkillRequirement('Mining', 40), ], [], 6),
|
||||||
|
LocationRow('Smelt a Bronze Bar', 'smithing', ['Bronze Ores', 'Furnace', ], [SkillRequirement('Smithing', 1), SkillRequirement('Mining', 1), ], [], 0),
|
||||||
LocationRow('Smelt an Iron Bar', 'smithing', ['Iron Ore', 'Furnace', ], [SkillRequirement('Smithing', 15), SkillRequirement('Mining', 15), ], [], 0),
|
LocationRow('Smelt an Iron Bar', 'smithing', ['Iron Ore', 'Furnace', ], [SkillRequirement('Smithing', 15), SkillRequirement('Mining', 15), ], [], 0),
|
||||||
LocationRow('Smelt a Silver Bar', 'smithing', ['Silver Ore', 'Furnace', ], [SkillRequirement('Smithing', 20), SkillRequirement('Mining', 20), ], [], 0),
|
LocationRow('Smelt a Silver Bar', 'smithing', ['Silver Ore', 'Furnace', ], [SkillRequirement('Smithing', 20), SkillRequirement('Mining', 20), ], [], 0),
|
||||||
LocationRow('Smelt a Steel Bar', 'smithing', ['Coal Ore', 'Iron Ore', 'Furnace', ], [SkillRequirement('Smithing', 30), SkillRequirement('Mining', 30), ], [], 2),
|
LocationRow('Smelt a Steel Bar', 'smithing', ['Coal Ore', 'Iron Ore', 'Furnace', ], [SkillRequirement('Smithing', 30), SkillRequirement('Mining', 30), ], [], 2),
|
||||||
LocationRow('Smelt a Gold Bar', 'smithing', ['Gold Ore', 'Furnace', ], [SkillRequirement('Smithing', 40), SkillRequirement('Mining', 40), ], [], 6),
|
LocationRow('Smelt a Gold Bar', 'smithing', ['Gold Ore', 'Furnace', ], [SkillRequirement('Smithing', 40), SkillRequirement('Mining', 40), ], [], 6),
|
||||||
|
LocationRow('Catch a Sardine', 'fishing', ['Shrimp Spot', ], [SkillRequirement('Fishing', 5), ], [], 0),
|
||||||
LocationRow('Catch some Anchovies', 'fishing', ['Shrimp Spot', ], [SkillRequirement('Fishing', 15), ], [], 0),
|
LocationRow('Catch some Anchovies', 'fishing', ['Shrimp Spot', ], [SkillRequirement('Fishing', 15), ], [], 0),
|
||||||
LocationRow('Catch a Trout', 'fishing', ['Fly Fishing Spot', ], [SkillRequirement('Fishing', 20), ], [], 0),
|
LocationRow('Catch a Trout', 'fishing', ['Fly Fishing Spot', ], [SkillRequirement('Fishing', 20), ], [], 0),
|
||||||
LocationRow('Prepare a Tetra', 'fishing', ['Camdozaal', ], [SkillRequirement('Fishing', 33), SkillRequirement('Cooking', 33), ], [], 2),
|
LocationRow('Prepare a Tetra', 'fishing', ['Camdozaal', ], [SkillRequirement('Fishing', 33), SkillRequirement('Cooking', 33), ], [], 2),
|
||||||
@@ -58,13 +77,16 @@ location_rows = [
|
|||||||
LocationRow('Bake a Redberry Pie', 'cooking', ['Redberry Bush', 'Wheat', 'Windmill', 'Pie Dish', ], [SkillRequirement('Cooking', 10), ], [], 0),
|
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('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', 32), ], [], 2),
|
LocationRow('Bake an Apple Pie', 'cooking', ['Cooking Apple', 'Wheat', 'Windmill', 'Pie Dish', ], [SkillRequirement('Cooking', 32), ], [], 2),
|
||||||
|
LocationRow('Enter the Cook\'s Guild', 'cooking', ['Cook\'s Guild', ], [], [], 0),
|
||||||
LocationRow('Bake a Cake', 'cooking', ['Wheat', 'Windmill', 'Egg', 'Milk', 'Cake Tin', ], [SkillRequirement('Cooking', 40), ], [], 6),
|
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('Bake a Meat Pizza', 'cooking', ['Wheat', 'Windmill', 'Cheese', 'Tomato', 'Meat', ], [SkillRequirement('Cooking', 45), ], [], 8),
|
||||||
|
LocationRow('Burn a Log', 'firemaking', [], [SkillRequirement('Firemaking', 1), SkillRequirement('Woodcutting', 1), ], [], 0),
|
||||||
LocationRow('Burn some Oak Logs', 'firemaking', ['Oak Tree', ], [SkillRequirement('Firemaking', 15), SkillRequirement('Woodcutting', 15), ], [], 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('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('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 an Oak Log', 'woodcutting', ['Oak Tree', ], [SkillRequirement('Woodcutting', 15), ], [], 0),
|
||||||
LocationRow('Cut a Willow Log', 'woodcutting', ['Willow Tree', ], [SkillRequirement('Woodcutting', 30), ], [], 0),
|
LocationRow('Cut a Willow Log', 'woodcutting', ['Willow Tree', ], [SkillRequirement('Woodcutting', 30), ], [], 0),
|
||||||
|
LocationRow('Kill a Duck', 'combat', ['Duck', ], [SkillRequirement('Combat', 1), ], [], 0),
|
||||||
LocationRow('Kill Jeff', 'combat', ['Dwarven Mountain Pass', ], [SkillRequirement('Combat', 2), ], [], 0),
|
LocationRow('Kill Jeff', 'combat', ['Dwarven Mountain Pass', ], [SkillRequirement('Combat', 2), ], [], 0),
|
||||||
LocationRow('Kill a Goblin', 'combat', ['Goblin', ], [SkillRequirement('Combat', 2), ], [], 0),
|
LocationRow('Kill a Goblin', 'combat', ['Goblin', ], [SkillRequirement('Combat', 2), ], [], 0),
|
||||||
LocationRow('Kill a Monkey', 'combat', ['Karamja', ], [SkillRequirement('Combat', 3), ], [], 0),
|
LocationRow('Kill a Monkey', 'combat', ['Karamja', ], [SkillRequirement('Combat', 3), ], [], 0),
|
||||||
@@ -81,19 +103,24 @@ location_rows = [
|
|||||||
LocationRow('Kill an Ogress Shaman', 'combat', ['Corsair Cove', ], [SkillRequirement('Combat', 82), ], [], 8),
|
LocationRow('Kill an Ogress Shaman', 'combat', ['Corsair Cove', ], [SkillRequirement('Combat', 82), ], [], 8),
|
||||||
LocationRow('Kill Obor', 'combat', ['Edgeville', ], [SkillRequirement('Combat', 106), ], [], 28),
|
LocationRow('Kill Obor', 'combat', ['Edgeville', ], [SkillRequirement('Combat', 106), ], [], 28),
|
||||||
LocationRow('Kill Bryophyta', 'combat', ['Central Varrock', ], [SkillRequirement('Combat', 128), ], [], 28),
|
LocationRow('Kill Bryophyta', 'combat', ['Central Varrock', ], [SkillRequirement('Combat', 128), ], [], 28),
|
||||||
|
LocationRow('Die', 'general', [], [], [], 0),
|
||||||
|
LocationRow('Reach a Level 10', 'general', [], [], [], 0),
|
||||||
LocationRow('Total XP 5,000', 'general', [], [], [], 0),
|
LocationRow('Total XP 5,000', 'general', [], [], [], 0),
|
||||||
LocationRow('Combat Level 5', 'general', [], [], [], 0),
|
LocationRow('Combat Level 5', 'general', [], [], [], 0),
|
||||||
LocationRow('Total XP 10,000', 'general', [], [], [], 0),
|
LocationRow('Total XP 10,000', 'general', [], [], [], 0),
|
||||||
LocationRow('Total Level 50', 'general', [], [], [], 0),
|
LocationRow('Total Level 50', 'general', [], [], [], 0),
|
||||||
|
LocationRow('Reach a Level 20', 'general', [], [], [], 0),
|
||||||
LocationRow('Total XP 25,000', 'general', [], [], [], 0),
|
LocationRow('Total XP 25,000', 'general', [], [], [], 0),
|
||||||
LocationRow('Total Level 100', 'general', [], [], [], 0),
|
LocationRow('Total Level 100', 'general', [], [], [], 0),
|
||||||
LocationRow('Total XP 50,000', 'general', [], [], [], 0),
|
LocationRow('Total XP 50,000', 'general', [], [], [], 0),
|
||||||
LocationRow('Combat Level 15', 'general', [], [], [], 0),
|
LocationRow('Combat Level 15', 'general', [], [], [], 0),
|
||||||
LocationRow('Total Level 150', 'general', [], [], [], 2),
|
LocationRow('Total Level 150', 'general', [], [], [], 2),
|
||||||
|
LocationRow('Reach a Level 30', 'general', [], [], [], 2),
|
||||||
LocationRow('Total XP 75,000', 'general', [], [], [], 2),
|
LocationRow('Total XP 75,000', 'general', [], [], [], 2),
|
||||||
LocationRow('Combat Level 25', 'general', [], [], [], 2),
|
LocationRow('Combat Level 25', 'general', [], [], [], 2),
|
||||||
LocationRow('Total XP 100,000', 'general', [], [], [], 6),
|
LocationRow('Total XP 100,000', 'general', [], [], [], 6),
|
||||||
LocationRow('Total Level 200', 'general', [], [], [], 6),
|
LocationRow('Total Level 200', 'general', [], [], [], 6),
|
||||||
|
LocationRow('Reach a Level 40', 'general', [], [], [], 6),
|
||||||
LocationRow('Total XP 125,000', 'general', [], [], [], 6),
|
LocationRow('Total XP 125,000', 'general', [], [], [], 6),
|
||||||
LocationRow('Combat Level 30', 'general', [], [], [], 10),
|
LocationRow('Combat Level 30', 'general', [], [], [], 10),
|
||||||
LocationRow('Total Level 250', 'general', [], [], [], 10),
|
LocationRow('Total Level 250', 'general', [], [], [], 10),
|
||||||
@@ -103,6 +130,28 @@ location_rows = [
|
|||||||
LocationRow('Open a Simple Lockbox', 'general', ['Camdozaal', ], [], [], 0),
|
LocationRow('Open a Simple Lockbox', 'general', ['Camdozaal', ], [], [], 0),
|
||||||
LocationRow('Open an Elaborate Lockbox', 'general', ['Camdozaal', ], [], [], 0),
|
LocationRow('Open an Elaborate Lockbox', 'general', ['Camdozaal', ], [], [], 0),
|
||||||
LocationRow('Open an Ornate Lockbox', 'general', ['Camdozaal', ], [], [], 0),
|
LocationRow('Open an Ornate Lockbox', 'general', ['Camdozaal', ], [], [], 0),
|
||||||
|
LocationRow('Trans your Gender', 'general', ['Makeover', ], [], [], 0),
|
||||||
|
LocationRow('Read a Flyer from Ali the Leaflet Dropper', 'general', ['Al Kharid', 'South of Varrock', ], [], [], 0),
|
||||||
|
LocationRow('Cry by the Members Gate to Taverley', 'general', ['Dwarven Mountain Pass', ], [], [], 0),
|
||||||
|
LocationRow('Get Prompted to Buy Membership', 'general', [], [], [], 0),
|
||||||
|
LocationRow('Pet the Stray Dog in Varrock', 'general', ['Central Varrock', 'West Varrock', 'South of Varrock', ], [], [], 0),
|
||||||
|
LocationRow('Get Sent to Jail in Shantay Pass', 'general', ['Al Kharid', 'Port Sarim', ], [], [], 0),
|
||||||
|
LocationRow('Have the Apothecary Make a Strength Potion', 'general', ['Central Varrock', 'Red Spider Eggs', 'Limpwurt Root', ], [], [], 0),
|
||||||
|
LocationRow('Put a Whole Banana into a Bottle of Karamjan Rum', 'general', ['Karamja', ], [], [], 0),
|
||||||
|
LocationRow('Attempt to Shear "The Thing"', 'general', ['Lumbridge Farms West', ], [], [], 0),
|
||||||
|
LocationRow('Eat a Kebab', 'general', ['Al Kharid', ], [], [], 0),
|
||||||
|
LocationRow('Return a Beer Glass to a Bar', 'general', ['Falador', ], [], [], 0),
|
||||||
|
LocationRow('Enter the Varrock Bear Cage', 'general', ['Varrock Palace', ], [], [], 0),
|
||||||
|
LocationRow('Equip a Cabbage Cape', 'general', ['Draynor Village', ], [], [], 0),
|
||||||
|
LocationRow('Equip a Pride Scarf', 'general', ['Draynor Village', ], [], [], 0),
|
||||||
|
LocationRow('Visit the Black Hole', 'general', ['Draynor Village', 'Dwarven Mines', ], [], [], 0),
|
||||||
|
LocationRow('Try to Equip Goblin Mail', 'general', ['Goblin', ], [], [], 0),
|
||||||
|
LocationRow('Equip an Orange Cape', 'general', ['Draynor Village', ], [], [], 0),
|
||||||
|
LocationRow('Find a Needle in a Haystack', 'general', ['Haystack', ], [], [], 0),
|
||||||
|
LocationRow('Insult the Homeless (but not Charlie he\'s cool)', 'general', ['Central Varrock', 'South of Varrock', ], [], [], 0),
|
||||||
|
LocationRow('Dance with Party Pete', 'general', ['Falador', ], [], [], 0),
|
||||||
|
LocationRow('Read a Newspaper', 'general', ['Central Varrock', ], [], [], 0),
|
||||||
|
LocationRow('Add a Card to the Chronicle', 'general', ['Draynor Village', ], [], [], 0),
|
||||||
LocationRow('Points: Cook\'s Assistant', 'points', [], [], [], 0),
|
LocationRow('Points: Cook\'s Assistant', 'points', [], [], [], 0),
|
||||||
LocationRow('Points: Demon Slayer', 'points', [], [], [], 0),
|
LocationRow('Points: Demon Slayer', 'points', [], [], [], 0),
|
||||||
LocationRow('Points: The Restless Ghost', 'points', [], [], [], 0),
|
LocationRow('Points: The Restless Ghost', 'points', [], [], [], 0),
|
||||||
|
|||||||
@@ -4,19 +4,19 @@ This file was auto generated by LogicCSVToPython.py
|
|||||||
from ..Regions import RegionRow
|
from ..Regions import RegionRow
|
||||||
|
|
||||||
region_rows = [
|
region_rows = [
|
||||||
RegionRow('Lumbridge', 'Area: Lumbridge', ['Lumbridge Farms East', 'Lumbridge Farms West', 'Al Kharid', 'Lumbridge Swamp', 'HAM Hideout', 'South of Varrock', 'Barbarian Village', 'Edgeville', 'Wilderness', ], ['Mind Runes', 'Spinning Wheel', 'Furnace', 'Chisel', 'Bronze Anvil', 'Fly Fishing Spot', 'Bowl', 'Cake Tin', 'Oak Tree', 'Willow Tree', 'Canoe Tree', 'Goblin', 'Imps', ]),
|
RegionRow('Lumbridge', 'Area: Lumbridge', ['Lumbridge Farms East', 'Lumbridge Farms West', 'Al Kharid', 'Lumbridge Swamp', 'HAM Hideout', 'South of Varrock', 'Barbarian Village', 'Edgeville', 'Wilderness', ], ['Mind Runes', 'Spinning Wheel', 'Furnace', 'Chisel', 'Bronze Anvil', 'Fly Fishing Spot', 'Bowl', 'Cake Tin', 'Oak Tree', 'Willow Tree', 'Canoe Tree', 'Goblin', 'Imps', 'Duck', 'Bar', ]),
|
||||||
RegionRow('Lumbridge Swamp', 'Area: Lumbridge Swamp', ['Lumbridge', 'HAM Hideout', ], ['Bronze Ores', 'Coal Ore', 'Shrimp Spot', 'Meat', 'Goblin', 'Imps', ]),
|
RegionRow('Lumbridge Swamp', 'Area: Lumbridge Swamp', ['Lumbridge', 'HAM Hideout', ], ['Bronze Ores', 'Coal Ore', 'Shrimp Spot', 'Meat', 'Goblin', 'Imps', 'Big Bones', 'Duck', ]),
|
||||||
RegionRow('HAM Hideout', 'Area: HAM Hideout', ['Lumbridge Farms West', 'Lumbridge', 'Lumbridge Swamp', 'Draynor Village', ], ['Goblin', ]),
|
RegionRow('HAM Hideout', 'Area: HAM Hideout', ['Lumbridge Farms West', 'Lumbridge', 'Lumbridge Swamp', 'Draynor Village', ], ['Goblin', ]),
|
||||||
RegionRow('Lumbridge Farms West', 'Area: Lumbridge Farms', ['Sourhog\'s Lair', 'HAM Hideout', 'Draynor Village', ], ['Sheep', 'Meat', 'Wheat', 'Windmill', 'Egg', 'Milk', 'Willow Tree', 'Imps', 'Potato', ]),
|
RegionRow('Lumbridge Farms West', 'Area: Lumbridge Farms', ['Sourhog\'s Lair', 'HAM Hideout', 'Draynor Village', ], ['Sheep', 'Meat', 'Wheat', 'Windmill', 'Egg', 'Milk', 'Willow Tree', 'Imps', 'Potato', 'Haystack', ]),
|
||||||
RegionRow('Lumbridge Farms East', 'Area: Lumbridge Farms', ['South of Varrock', 'Lumbridge', ], ['Meat', 'Egg', 'Milk', 'Willow Tree', 'Goblin', 'Imps', 'Potato', ]),
|
RegionRow('Lumbridge Farms East', 'Area: Lumbridge Farms', ['South of Varrock', 'Lumbridge', ], ['Meat', 'Egg', 'Milk', 'Willow Tree', 'Goblin', 'Imps', 'Potato', ]),
|
||||||
RegionRow('Sourhog\'s Lair', 'Area: South of Varrock', ['Lumbridge Farms West', 'Draynor Manor Outskirts', ], ['', ]),
|
RegionRow('Sourhog\'s Lair', 'Area: South of Varrock', ['Lumbridge Farms West', 'Draynor Manor Outskirts', ], ['', ]),
|
||||||
RegionRow('South of Varrock', 'Area: South of Varrock', ['Al Kharid', 'West Varrock', 'Central Varrock', 'East Varrock', 'Lumbridge Farms East', 'Lumbridge', 'Barbarian Village', 'Edgeville', 'Wilderness', ], ['Sheep', 'Bronze Ores', 'Iron Ore', 'Silver Ore', 'Redberry Bush', 'Meat', 'Wheat', 'Oak Tree', 'Willow Tree', 'Canoe Tree', 'Guard', 'Imps', 'Clay Ore', ]),
|
RegionRow('South of Varrock', 'Area: South of Varrock', ['Al Kharid', 'West Varrock', 'Central Varrock', 'Lumberyard', 'Lumbridge Farms East', 'Lumbridge', 'Barbarian Village', 'Edgeville', 'Wilderness', ], ['Sheep', 'Bronze Ores', 'Iron Ore', 'Silver Ore', 'Redberry Bush', 'Meat', 'Wheat', 'Oak Tree', 'Willow Tree', 'Canoe Tree', 'Guard', 'Imps', 'Clay Ore', 'Duck', ]),
|
||||||
RegionRow('East Varrock', 'Area: East Varrock', ['Wilderness', 'South of Varrock', 'Central Varrock', 'Varrock Palace', ], ['Guard', ]),
|
RegionRow('Lumberyard', 'Area: Lumberyard', ['Wilderness', 'South of Varrock', 'Central Varrock', 'Varrock Palace', ], ['Guard', 'Bar', ]),
|
||||||
RegionRow('Central Varrock', 'Area: Central Varrock', ['Varrock Palace', 'East Varrock', 'South of Varrock', 'West Varrock', ], ['Mind Runes', 'Chisel', 'Anvil', 'Bowl', 'Cake Tin', 'Oak Tree', 'Barbarian', 'Guard', 'Rune Essence', 'Imps', ]),
|
RegionRow('Central Varrock', 'Area: Central Varrock', ['Varrock Palace', 'Lumberyard', 'South of Varrock', 'West Varrock', ], ['Mind Runes', 'Chisel', 'Anvil', 'Bowl', 'Cake Tin', 'Oak Tree', 'Barbarian', 'Guard', 'Rune Essence', 'Imps', 'Makeover', 'Bar', ]),
|
||||||
RegionRow('Varrock Palace', 'Area: Varrock Palace', ['Wilderness', 'East Varrock', 'Central Varrock', 'West Varrock', ], ['Pie Dish', 'Oak Tree', 'Zombie', 'Guard', 'Deadly Red Spider', 'Moss Giant', 'Nature Runes', 'Law Runes', ]),
|
RegionRow('Varrock Palace', 'Area: Varrock Palace', ['Wilderness', 'Lumberyard', 'Central Varrock', 'West Varrock', ], ['Pie Dish', 'Oak Tree', 'Zombie', 'Guard', 'Deadly Red Spider', 'Moss Giant', 'Nature Runes', 'Law Runes', 'Big Bones', 'Makeover', 'Red Spider Eggs', ]),
|
||||||
RegionRow('West Varrock', 'Area: West Varrock', ['Wilderness', 'Varrock Palace', 'South of Varrock', 'Barbarian Village', 'Edgeville', 'Cook\'s Guild', ], ['Anvil', 'Wheat', 'Oak Tree', 'Goblin', 'Guard', 'Onion', ]),
|
RegionRow('West Varrock', 'Area: West Varrock', ['Wilderness', 'Varrock Palace', 'South of Varrock', 'Barbarian Village', 'Edgeville', 'Cook\'s Guild', ], ['Anvil', 'Wheat', 'Oak Tree', 'Goblin', 'Guard', 'Onion', ]),
|
||||||
RegionRow('Cook\'s Guild', 'Area: West Varrock*', ['West Varrock', ], ['Bowl', 'Cooking Apple', 'Pie Dish', 'Cake Tin', 'Windmill', ]),
|
RegionRow('Cook\'s Guild', 'Area: West Varrock*', ['West Varrock', ], ['Bowl', 'Cooking Apple', 'Pie Dish', 'Cake Tin', 'Windmill', ]),
|
||||||
RegionRow('Edgeville', 'Area: Edgeville', ['Wilderness', 'West Varrock', 'Barbarian Village', 'South of Varrock', 'Lumbridge', ], ['Furnace', 'Chisel', 'Bronze Ores', 'Iron Ore', 'Coal Ore', 'Bowl', 'Meat', 'Cake Tin', 'Willow Tree', 'Canoe Tree', 'Zombie', 'Guard', 'Hill Giant', 'Nature Runes', 'Law Runes', 'Imps', ]),
|
RegionRow('Edgeville', 'Area: Edgeville', ['Wilderness', 'West Varrock', 'Barbarian Village', 'South of Varrock', 'Lumbridge', ], ['Furnace', 'Chisel', 'Bronze Ores', 'Iron Ore', 'Coal Ore', 'Bowl', 'Meat', 'Cake Tin', 'Willow Tree', 'Canoe Tree', 'Zombie', 'Guard', 'Hill Giant', 'Nature Runes', 'Law Runes', 'Imps', 'Big Bones', 'Limpwurt Root', 'Haystack', ]),
|
||||||
RegionRow('Barbarian Village', 'Area: Barbarian Village', ['Edgeville', 'West Varrock', 'Draynor Manor Outskirts', 'Dwarven Mountain Pass', ], ['Spinning Wheel', 'Coal Ore', 'Anvil', 'Fly Fishing Spot', 'Meat', 'Canoe Tree', 'Barbarian', 'Zombie', 'Law Runes', ]),
|
RegionRow('Barbarian Village', 'Area: Barbarian Village', ['Edgeville', 'West Varrock', 'Draynor Manor Outskirts', 'Dwarven Mountain Pass', ], ['Spinning Wheel', 'Coal Ore', 'Anvil', 'Fly Fishing Spot', 'Meat', 'Canoe Tree', 'Barbarian', 'Zombie', 'Law Runes', ]),
|
||||||
RegionRow('Draynor Manor Outskirts', 'Area: Draynor Manor', ['Barbarian Village', 'Sourhog\'s Lair', 'Draynor Village', 'Falador East Outskirts', ], ['Goblin', ]),
|
RegionRow('Draynor Manor Outskirts', 'Area: Draynor Manor', ['Barbarian Village', 'Sourhog\'s Lair', 'Draynor Village', 'Falador East Outskirts', ], ['Goblin', ]),
|
||||||
RegionRow('Draynor Manor', 'Area: Draynor Manor', ['Draynor Village', ], ['', ]),
|
RegionRow('Draynor Manor', 'Area: Draynor Manor', ['Draynor Village', ], ['', ]),
|
||||||
@@ -27,21 +27,21 @@ region_rows = [
|
|||||||
RegionRow('Ice Mountain', 'Area: Ice Mountain', ['Wilderness', 'Monastery', 'Dwarven Mines', 'Camdozaal*', ], ['', ]),
|
RegionRow('Ice Mountain', 'Area: Ice Mountain', ['Wilderness', 'Monastery', 'Dwarven Mines', 'Camdozaal*', ], ['', ]),
|
||||||
RegionRow('Camdozaal', 'Area: Ice Mountain', ['Ice Mountain', ], ['Clay Ore', ]),
|
RegionRow('Camdozaal', 'Area: Ice Mountain', ['Ice Mountain', ], ['Clay Ore', ]),
|
||||||
RegionRow('Monastery', 'Area: Monastery', ['Wilderness', 'Dwarven Mountain Pass', 'Dwarven Mines', 'Ice Mountain', ], ['Sheep', ]),
|
RegionRow('Monastery', 'Area: Monastery', ['Wilderness', 'Dwarven Mountain Pass', 'Dwarven Mines', 'Ice Mountain', ], ['Sheep', ]),
|
||||||
RegionRow('Falador', 'Area: Falador', ['Dwarven Mountain Pass', 'Falador Farms', 'Dwarven Mines', ], ['Furnace', 'Chisel', 'Bowl', 'Cake Tin', 'Oak Tree', 'Guard', 'Imps', ]),
|
RegionRow('Falador', 'Area: Falador', ['Dwarven Mountain Pass', 'Falador Farms', 'Dwarven Mines', ], ['Furnace', 'Chisel', 'Bowl', 'Cake Tin', 'Oak Tree', 'Guard', 'Imps', 'Duck', 'Makeover', 'Bar', ]),
|
||||||
RegionRow('Falador Farms', 'Area: Falador Farms', ['Falador', 'Falador East Outskirts', 'Draynor Village', 'Port Sarim', 'Rimmington', 'Crafting Guild Outskirts', ], ['Spinning Wheel', 'Meat', 'Egg', 'Milk', 'Oak Tree', 'Imps', ]),
|
RegionRow('Falador Farms', 'Area: Falador Farms', ['Falador', 'Falador East Outskirts', 'Draynor Village', 'Port Sarim', 'Rimmington', 'Crafting Guild Outskirts', ], ['Spinning Wheel', 'Meat', 'Egg', 'Milk', 'Oak Tree', 'Imps', 'Duck', ]),
|
||||||
RegionRow('Port Sarim', 'Area: Port Sarim', ['Falador Farms', 'Mudskipper Point', 'Rimmington', 'Karamja Docks', 'Crandor', ], ['Mind Runes', 'Shrimp Spot', 'Meat', 'Cheese', 'Tomato', 'Oak Tree', 'Willow Tree', 'Goblin', 'Potato', ]),
|
RegionRow('Port Sarim', 'Area: Port Sarim', ['Falador Farms', 'Mudskipper Point', 'Rimmington', 'Karamja Docks', 'Crandor', ], ['Mind Runes', 'Shrimp Spot', 'Meat', 'Cheese', 'Tomato', 'Oak Tree', 'Willow Tree', 'Goblin', 'Potato', ]),
|
||||||
RegionRow('Karamja Docks', 'Area: Mudskipper Point', ['Port Sarim', 'Karamja', ], ['', ]),
|
RegionRow('Karamja Docks', 'Area: Mudskipper Point', ['Port Sarim', 'Karamja', ], ['', ]),
|
||||||
RegionRow('Mudskipper Point', 'Area: Mudskipper Point', ['Rimmington', 'Port Sarim', ], ['Anvil', 'Ice Giant', 'Nature Runes', 'Law Runes', ]),
|
RegionRow('Mudskipper Point', 'Area: Mudskipper Point', ['Rimmington', 'Port Sarim', ], ['Anvil', 'Ice Giant', 'Nature Runes', 'Law Runes', 'Big Bones', 'Limpwurt Root', ]),
|
||||||
RegionRow('Karamja', 'Area: Karamja', ['Karamja Docks', 'Crandor', ], ['Gold Ore', 'Lobster Spot', 'Bowl', 'Cake Tin', 'Deadly Red Spider', 'Imps', ]),
|
RegionRow('Karamja', 'Area: Karamja', ['Karamja Docks', 'Crandor', ], ['Gold Ore', 'Lobster Spot', 'Bowl', 'Cake Tin', 'Deadly Red Spider', 'Imps', 'Red Spider Eggs', ]),
|
||||||
RegionRow('Crandor', 'Area: Crandor', ['Karamja', 'Port Sarim', ], ['Coal Ore', 'Gold Ore', 'Moss Giant', 'Lesser Demon', 'Nature Runes', 'Law Runes', ]),
|
RegionRow('Crandor', 'Area: Crandor', ['Karamja', 'Port Sarim', ], ['Coal Ore', 'Gold Ore', 'Moss Giant', 'Lesser Demon', 'Nature Runes', 'Law Runes', 'Big Bones', 'Limpwurt Root', ]),
|
||||||
RegionRow('Rimmington', 'Area: Rimmington', ['Falador Farms', 'Port Sarim', 'Mudskipper Point', 'Crafting Guild Peninsula', 'Corsair Cove', ], ['Chisel', 'Bronze Ores', 'Iron Ore', 'Gold Ore', 'Bowl', 'Cake Tin', 'Wheat', 'Oak Tree', 'Willow Tree', 'Crafting Moulds', 'Imps', 'Clay Ore', 'Onion', ]),
|
RegionRow('Rimmington', 'Area: Rimmington', ['Falador Farms', 'Port Sarim', 'Mudskipper Point', 'Crafting Guild Peninsula', 'Corsair Cove', ], ['Chisel', 'Bronze Ores', 'Iron Ore', 'Gold Ore', 'Bowl', 'Cake Tin', 'Wheat', 'Oak Tree', 'Willow Tree', 'Crafting Moulds', 'Imps', 'Clay Ore', 'Onion', ]),
|
||||||
RegionRow('Crafting Guild Peninsula', 'Area: Crafting Guild', ['Falador Farms', 'Rimmington', ], ['', ]),
|
RegionRow('Crafting Guild Peninsula', 'Area: Crafting Guild', ['Falador Farms', 'Rimmington', ], ['Limpwurt Root', ]),
|
||||||
RegionRow('Crafting Guild Outskirts', 'Area: Crafting Guild', ['Falador Farms', 'Crafting Guild', ], ['Sheep', 'Willow Tree', 'Oak Tree', ]),
|
RegionRow('Crafting Guild Outskirts', 'Area: Crafting Guild', ['Falador Farms', 'Crafting Guild', ], ['Sheep', 'Willow Tree', 'Oak Tree', 'Makeover', ]),
|
||||||
RegionRow('Crafting Guild', 'Area: Crafting Guild*', ['Crafting Guild', ], ['Spinning Wheel', 'Chisel', 'Silver Ore', 'Gold Ore', 'Meat', 'Milk', 'Clay Ore', ]),
|
RegionRow('Crafting Guild', 'Area: Crafting Guild*', ['Crafting Guild', ], ['Spinning Wheel', 'Chisel', 'Silver Ore', 'Gold Ore', 'Meat', 'Milk', 'Clay Ore', ]),
|
||||||
RegionRow('Draynor Village', 'Area: Draynor Village', ['Draynor Manor', 'Lumbridge Farms West', 'HAM Hideout', 'Wizard Tower', ], ['Anvil', 'Shrimp Spot', 'Wheat', 'Cheese', 'Tomato', 'Willow Tree', 'Goblin', 'Zombie', 'Nature Runes', 'Law Runes', 'Imps', ]),
|
RegionRow('Draynor Village', 'Area: Draynor Village', ['Draynor Manor', 'Lumbridge Farms West', 'HAM Hideout', 'Wizard Tower', ], ['Anvil', 'Shrimp Spot', 'Wheat', 'Cheese', 'Tomato', 'Willow Tree', 'Goblin', 'Zombie', 'Nature Runes', 'Law Runes', 'Imps', ]),
|
||||||
RegionRow('Wizard Tower', 'Area: Wizard Tower', ['Draynor Village', ], ['Lesser Demon', 'Rune Essence', ]),
|
RegionRow('Wizard Tower', 'Area: Wizard Tower', ['Draynor Village', ], ['Lesser Demon', 'Rune Essence', ]),
|
||||||
RegionRow('Corsair Cove', 'Area: Corsair Cove*', ['Rimmington', ], ['Anvil', 'Meat', ]),
|
RegionRow('Corsair Cove', 'Area: Corsair Cove*', ['Rimmington', ], ['Anvil', 'Meat', 'Limpwurt Root', ]),
|
||||||
RegionRow('Al Kharid', 'Area: Al Kharid', ['South of Varrock', 'Citharede Abbey', 'Lumbridge', 'Port Sarim', ], ['Furnace', 'Chisel', 'Bronze Ores', 'Iron Ore', 'Silver Ore', 'Coal Ore', 'Gold Ore', 'Shrimp Spot', 'Bowl', 'Cake Tin', 'Cheese', 'Crafting Moulds', 'Imps', ]),
|
RegionRow('Al Kharid', 'Area: Al Kharid', ['South of Varrock', 'Citharede Abbey', 'Lumbridge', 'Port Sarim', ], ['Furnace', 'Chisel', 'Bronze Ores', 'Iron Ore', 'Silver Ore', 'Coal Ore', 'Gold Ore', 'Shrimp Spot', 'Bowl', 'Cake Tin', 'Cheese', 'Crafting Moulds', 'Imps', ]),
|
||||||
RegionRow('Citharede Abbey', 'Area: Citharede Abbey', ['Al Kharid', ], ['Iron Ore', 'Coal Ore', 'Anvil', 'Hill Giant', 'Nature Runes', 'Law Runes', ]),
|
RegionRow('Citharede Abbey', 'Area: Citharede Abbey', ['Al Kharid', ], ['Iron Ore', 'Coal Ore', 'Anvil', 'Hill Giant', 'Nature Runes', 'Law Runes', 'Big Bones', 'Limpwurt Root', ]),
|
||||||
RegionRow('Wilderness', 'Area: Wilderness', ['East Varrock', 'Varrock Palace', 'West Varrock', 'Edgeville', 'Monastery', 'Ice Mountain', 'Goblin Village', 'South of Varrock', 'Lumbridge', ], ['Furnace', 'Chisel', 'Iron Ore', 'Coal Ore', 'Anvil', 'Meat', 'Cake Tin', 'Cheese', 'Tomato', 'Oak Tree', 'Canoe Tree', 'Zombie', 'Hill Giant', 'Deadly Red Spider', 'Moss Giant', 'Ice Giant', 'Lesser Demon', 'Nature Runes', 'Law Runes', ]),
|
RegionRow('Wilderness', 'Area: Wilderness', ['Lumberyard', 'Varrock Palace', 'West Varrock', 'Edgeville', 'Monastery', 'Ice Mountain', 'Goblin Village', 'South of Varrock', 'Lumbridge', ], ['Furnace', 'Chisel', 'Iron Ore', 'Coal Ore', 'Anvil', 'Meat', 'Cake Tin', 'Cheese', 'Tomato', 'Oak Tree', 'Canoe Tree', 'Zombie', 'Hill Giant', 'Deadly Red Spider', 'Moss Giant', 'Ice Giant', 'Lesser Demon', 'Nature Runes', 'Law Runes', 'Big Bones', 'Limpwurt Root', 'Bar', ]),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -51,4 +51,11 @@ resource_rows = [
|
|||||||
ResourceRow('Clay Ore'),
|
ResourceRow('Clay Ore'),
|
||||||
ResourceRow('Onion'),
|
ResourceRow('Onion'),
|
||||||
ResourceRow('Potato'),
|
ResourceRow('Potato'),
|
||||||
|
ResourceRow('Big Bones'),
|
||||||
|
ResourceRow('Duck'),
|
||||||
|
ResourceRow('Makeover'),
|
||||||
|
ResourceRow('Limpwurt Root'),
|
||||||
|
ResourceRow('Bar'),
|
||||||
|
ResourceRow('Haystack'),
|
||||||
|
ResourceRow('Red Spider Eggs'),
|
||||||
]
|
]
|
||||||
|
|||||||
@@ -73,7 +73,7 @@ class ItemNames(str, Enum):
|
|||||||
South_Of_Varrock = "Area: South of Varrock"
|
South_Of_Varrock = "Area: South of Varrock"
|
||||||
Central_Varrock = "Area: Central Varrock"
|
Central_Varrock = "Area: Central Varrock"
|
||||||
Varrock_Palace = "Area: Varrock Palace"
|
Varrock_Palace = "Area: Varrock Palace"
|
||||||
East_Of_Varrock = "Area: East Varrock"
|
Lumberyard = "Area: Lumberyard"
|
||||||
West_Varrock = "Area: West Varrock"
|
West_Varrock = "Area: West Varrock"
|
||||||
Edgeville = "Area: Edgeville"
|
Edgeville = "Area: Edgeville"
|
||||||
Barbarian_Village = "Area: Barbarian Village"
|
Barbarian_Village = "Area: Barbarian Village"
|
||||||
@@ -94,8 +94,8 @@ class ItemNames(str, Enum):
|
|||||||
Progressive_Weapons = "Progressive Weapons"
|
Progressive_Weapons = "Progressive Weapons"
|
||||||
Progressive_Tools = "Progressive Tools"
|
Progressive_Tools = "Progressive Tools"
|
||||||
Progressive_Range_Armor = "Progressive Ranged Armor"
|
Progressive_Range_Armor = "Progressive Ranged Armor"
|
||||||
Progressive_Range_Weapon = "Progressive Ranged Weapons"
|
Progressive_Range_Weapon = "Progressive Ranged Weapon"
|
||||||
Progressive_Magic = "Progressive Magic"
|
Progressive_Magic = "Progressive Magic Spell"
|
||||||
Lobsters = "10 Lobsters"
|
Lobsters = "10 Lobsters"
|
||||||
Swordfish = "5 Swordfish"
|
Swordfish = "5 Swordfish"
|
||||||
Energy_Potions = "10 Energy Potions"
|
Energy_Potions = "10 Energy Potions"
|
||||||
|
|||||||
@@ -3,18 +3,19 @@ from dataclasses import dataclass
|
|||||||
from Options import Choice, Toggle, Range, PerGameCommonOptions
|
from Options import Choice, Toggle, Range, PerGameCommonOptions
|
||||||
|
|
||||||
MAX_COMBAT_TASKS = 16
|
MAX_COMBAT_TASKS = 16
|
||||||
MAX_PRAYER_TASKS = 3
|
|
||||||
MAX_MAGIC_TASKS = 4
|
MAX_PRAYER_TASKS = 5
|
||||||
MAX_RUNECRAFT_TASKS = 3
|
MAX_MAGIC_TASKS = 7
|
||||||
MAX_CRAFTING_TASKS = 5
|
MAX_RUNECRAFT_TASKS = 8
|
||||||
MAX_MINING_TASKS = 5
|
MAX_CRAFTING_TASKS = 11
|
||||||
MAX_SMITHING_TASKS = 4
|
MAX_MINING_TASKS = 6
|
||||||
MAX_FISHING_TASKS = 5
|
MAX_SMITHING_TASKS = 5
|
||||||
MAX_COOKING_TASKS = 5
|
MAX_FISHING_TASKS = 6
|
||||||
MAX_FIREMAKING_TASKS = 2
|
MAX_COOKING_TASKS = 6
|
||||||
|
MAX_FIREMAKING_TASKS = 3
|
||||||
MAX_WOODCUTTING_TASKS = 3
|
MAX_WOODCUTTING_TASKS = 3
|
||||||
|
|
||||||
NON_QUEST_LOCATION_COUNT = 22
|
NON_QUEST_LOCATION_COUNT = 49
|
||||||
|
|
||||||
|
|
||||||
class StartingArea(Choice):
|
class StartingArea(Choice):
|
||||||
@@ -58,6 +59,31 @@ class ProgressiveTasks(Toggle):
|
|||||||
display_name = "Progressive Tasks"
|
display_name = "Progressive Tasks"
|
||||||
|
|
||||||
|
|
||||||
|
class EnableDuds(Toggle):
|
||||||
|
"""
|
||||||
|
Whether to include filler "Dud" items that serve no purpose but allow for more tasks in the pool.
|
||||||
|
"""
|
||||||
|
display_name = "Enable Duds"
|
||||||
|
|
||||||
|
|
||||||
|
class DudCount(Range):
|
||||||
|
"""
|
||||||
|
How many "Dud" items to include in the pool. This setting is ignored if "Enable Duds" is not included
|
||||||
|
"""
|
||||||
|
display_name = "Dud Item Count"
|
||||||
|
range_start = 0
|
||||||
|
range_end = 30
|
||||||
|
default = 10
|
||||||
|
|
||||||
|
|
||||||
|
class EnableCarePacks(Toggle):
|
||||||
|
"""
|
||||||
|
Whether or not to include useful "Care Pack" items that allow you to trade over specific items.
|
||||||
|
Note: Requires your account NOT to be an Ironman. Also, requires access to another account to trade over the items,
|
||||||
|
or gold to purchase off of the grand exchange.
|
||||||
|
"""
|
||||||
|
display_name = "Enable Care Packs"
|
||||||
|
|
||||||
class MaxCombatLevel(Range):
|
class MaxCombatLevel(Range):
|
||||||
"""
|
"""
|
||||||
The highest combat level of monster to possibly be assigned as a task.
|
The highest combat level of monster to possibly be assigned as a task.
|
||||||
@@ -472,6 +498,9 @@ class OSRSOptions(PerGameCommonOptions):
|
|||||||
starting_area: StartingArea
|
starting_area: StartingArea
|
||||||
brutal_grinds: BrutalGrinds
|
brutal_grinds: BrutalGrinds
|
||||||
progressive_tasks: ProgressiveTasks
|
progressive_tasks: ProgressiveTasks
|
||||||
|
enable_duds: EnableDuds
|
||||||
|
dud_count: DudCount
|
||||||
|
enable_carepacks: EnableCarePacks
|
||||||
max_combat_level: MaxCombatLevel
|
max_combat_level: MaxCombatLevel
|
||||||
max_combat_tasks: MaxCombatTasks
|
max_combat_tasks: MaxCombatTasks
|
||||||
combat_task_weight: CombatTaskWeight
|
combat_task_weight: CombatTaskWeight
|
||||||
|
|||||||
@@ -212,11 +212,14 @@ def get_skill_rule(skill, level, player, options) -> CollectionRule:
|
|||||||
return lambda state: True
|
return lambda state: True
|
||||||
|
|
||||||
|
|
||||||
def generate_special_rules_for(entrance, region_row, outbound_region_name, player, options):
|
def generate_special_rules_for(entrance, region_row, outbound_region_name, player, options, world):
|
||||||
if outbound_region_name == RegionNames.Cooks_Guild:
|
if outbound_region_name == RegionNames.Cooks_Guild:
|
||||||
add_rule(entrance, get_cooking_skill_rule(32, player, options))
|
add_rule(entrance, get_cooking_skill_rule(32, player, options))
|
||||||
|
# Since there's goblins in this chunk, checking for hat access is superfluous, you'd always have it anyway
|
||||||
elif outbound_region_name == RegionNames.Crafting_Guild:
|
elif outbound_region_name == RegionNames.Crafting_Guild:
|
||||||
add_rule(entrance, get_crafting_skill_rule(40, player, options))
|
add_rule(entrance, get_crafting_skill_rule(40, player, options))
|
||||||
|
# Literally the only brown apron access in the entirety of f2p is buying it in varrock
|
||||||
|
add_rule(entrance, lambda state: state.can_reach_region(RegionNames.Central_Varrock, player))
|
||||||
elif outbound_region_name == RegionNames.Corsair_Cove:
|
elif outbound_region_name == RegionNames.Corsair_Cove:
|
||||||
# Need to be able to start Corsair Curse in addition to having the item
|
# 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))
|
add_rule(entrance, lambda state: state.can_reach(RegionNames.Falador_Farm, "Region", player))
|
||||||
@@ -224,6 +227,17 @@ def generate_special_rules_for(entrance, region_row, outbound_region_name, playe
|
|||||||
add_rule(entrance, lambda state: state.has(ItemNames.QP_Below_Ice_Mountain, player))
|
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*":
|
elif region_row.name == "Dwarven Mountain Pass" and outbound_region_name == "Anvil*":
|
||||||
add_rule(entrance, lambda state: state.has(ItemNames.QP_Dorics_Quest, player))
|
add_rule(entrance, lambda state: state.has(ItemNames.QP_Dorics_Quest, player))
|
||||||
|
elif outbound_region_name == RegionNames.Crandor:
|
||||||
|
add_rule(entrance, lambda state: state.can_reach_region(RegionNames.South_Of_Varrock, player))
|
||||||
|
add_rule(entrance, lambda state: state.can_reach_region(RegionNames.Edgeville, player))
|
||||||
|
add_rule(entrance, lambda state: state.can_reach_region(RegionNames.Lumbridge, player))
|
||||||
|
add_rule(entrance, lambda state: state.can_reach_region(RegionNames.Rimmington, player))
|
||||||
|
add_rule(entrance, lambda state: state.can_reach_region(RegionNames.Monastery, player))
|
||||||
|
add_rule(entrance, lambda state: state.can_reach_region(RegionNames.Dwarven_Mines, player))
|
||||||
|
add_rule(entrance, lambda state: state.can_reach_region(RegionNames.Port_Sarim, player))
|
||||||
|
add_rule(entrance, lambda state: state.can_reach_region(RegionNames.Draynor_Village, player))
|
||||||
|
add_rule(entrance, lambda state: world.quest_points(state) >= 32)
|
||||||
|
|
||||||
|
|
||||||
# Special logic for canoes
|
# Special logic for canoes
|
||||||
canoe_regions = [RegionNames.Lumbridge, RegionNames.South_Of_Varrock, RegionNames.Barbarian_Village,
|
canoe_regions = [RegionNames.Lumbridge, RegionNames.South_Of_Varrock, RegionNames.Barbarian_Village,
|
||||||
|
|||||||
@@ -168,7 +168,7 @@ class OSRSWorld(World):
|
|||||||
|
|
||||||
item_name = self.region_rows_by_name[parsed_outbound].itemReq
|
item_name = self.region_rows_by_name[parsed_outbound].itemReq
|
||||||
entrance.access_rule = lambda state, item_name=item_name.replace("*",""): state.has(item_name, self.player)
|
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)
|
generate_special_rules_for(entrance, region_row, outbound_region_name, self.player, self.options, self)
|
||||||
|
|
||||||
for resource_region in region_row.resources:
|
for resource_region in region_row.resources:
|
||||||
if not resource_region:
|
if not resource_region:
|
||||||
@@ -179,7 +179,7 @@ class OSRSWorld(World):
|
|||||||
entrance.connect(self.region_name_to_data[resource_region])
|
entrance.connect(self.region_name_to_data[resource_region])
|
||||||
else:
|
else:
|
||||||
entrance.connect(self.region_name_to_data[resource_region.replace('*', '')])
|
entrance.connect(self.region_name_to_data[resource_region.replace('*', '')])
|
||||||
generate_special_rules_for(entrance, region_row, resource_region, self.player, self.options)
|
generate_special_rules_for(entrance, region_row, resource_region, self.player, self.options, self)
|
||||||
|
|
||||||
self.roll_locations()
|
self.roll_locations()
|
||||||
|
|
||||||
@@ -195,7 +195,16 @@ class OSRSWorld(World):
|
|||||||
generation_is_fake = hasattr(self.multiworld, "generation_is_fake") # UT specific override
|
generation_is_fake = hasattr(self.multiworld, "generation_is_fake") # UT specific override
|
||||||
locations_required = 0
|
locations_required = 0
|
||||||
for item_row in item_rows:
|
for item_row in item_rows:
|
||||||
|
# If it's a filler item, set it aside for later
|
||||||
|
if item_row.progression == ItemClassification.filler:
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If it starts with "Care Pack", only add it if Care Packs are enabled
|
||||||
|
if item_row.name.startswith("Care Pack"):
|
||||||
|
if not self.options.enable_carepacks:
|
||||||
|
continue
|
||||||
locations_required += item_row.amount
|
locations_required += item_row.amount
|
||||||
|
if self.options.enable_duds: locations_required += self.options.dud_count
|
||||||
|
|
||||||
locations_added = 1 # At this point we've already added the starting area, so we start at 1 instead of 0
|
locations_added = 1 # At this point we've already added the starting area, so we start at 1 instead of 0
|
||||||
|
|
||||||
@@ -232,6 +241,7 @@ class OSRSWorld(World):
|
|||||||
max_amount_for_task_type = getattr(self.options, f"max_{task_type}_tasks")
|
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]
|
tasks_for_this_type = [task for task in self.locations_by_category[task_type]
|
||||||
if self.task_within_skill_levels(task.skills)]
|
if self.task_within_skill_levels(task.skills)]
|
||||||
|
max_amount_for_task_type = min(max_amount_for_task_type, len(tasks_for_this_type))
|
||||||
if not self.options.progressive_tasks:
|
if not self.options.progressive_tasks:
|
||||||
rnd.shuffle(tasks_for_this_type)
|
rnd.shuffle(tasks_for_this_type)
|
||||||
else:
|
else:
|
||||||
@@ -286,16 +296,36 @@ class OSRSWorld(World):
|
|||||||
self.create_and_add_location(index)
|
self.create_and_add_location(index)
|
||||||
|
|
||||||
def create_items(self) -> None:
|
def create_items(self) -> None:
|
||||||
|
filler_items = []
|
||||||
for item_row in item_rows:
|
for item_row in item_rows:
|
||||||
if item_row.name != self.starting_area_item:
|
if item_row.name != self.starting_area_item:
|
||||||
|
# If it's a filler item, set it aside for later
|
||||||
|
if item_row.progression == ItemClassification.filler:
|
||||||
|
filler_items.append(item_row)
|
||||||
|
continue
|
||||||
|
|
||||||
|
# If it starts with "Care Pack", only add it if Care Packs are enabled
|
||||||
|
if item_row.name.startswith("Care Pack"):
|
||||||
|
if not self.options.enable_carepacks:
|
||||||
|
continue
|
||||||
|
|
||||||
for c in range(item_row.amount):
|
for c in range(item_row.amount):
|
||||||
item = self.create_item(item_row.name)
|
item = self.create_item(item_row.name)
|
||||||
self.multiworld.itempool.append(item)
|
self.multiworld.itempool.append(item)
|
||||||
|
if self.options.enable_duds:
|
||||||
|
self.random.shuffle(filler_items)
|
||||||
|
filler_items = filler_items[0:self.options.dud_count]
|
||||||
|
for item_row in filler_items:
|
||||||
|
item = self.create_item(item_row.name)
|
||||||
|
self.multiworld.itempool.append(item)
|
||||||
|
|
||||||
def get_filler_item_name(self) -> str:
|
def get_filler_item_name(self) -> str:
|
||||||
return self.random.choice(
|
if self.options.enable_duds:
|
||||||
[ItemNames.Progressive_Armor, ItemNames.Progressive_Weapons, ItemNames.Progressive_Magic,
|
return self.random.choice([item for item in item_rows if item.progression == ItemClassification.filler])
|
||||||
ItemNames.Progressive_Tools, ItemNames.Progressive_Range_Armor, ItemNames.Progressive_Range_Weapon])
|
else:
|
||||||
|
return self.random.choice([ItemNames.Progressive_Weapons, ItemNames.Progressive_Magic,
|
||||||
|
ItemNames.Progressive_Range_Weapon, ItemNames.Progressive_Armor,
|
||||||
|
ItemNames.Progressive_Range_Armor, ItemNames.Progressive_Tools])
|
||||||
|
|
||||||
def create_and_add_location(self, row_index) -> None:
|
def create_and_add_location(self, row_index) -> None:
|
||||||
location_row = location_rows[row_index]
|
location_row = location_rows[row_index]
|
||||||
|
|||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user