forked from mirror/Archipelago
Compare commits
112 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 30fa0658b0 | |||
| 44a0c44036 | |||
|
|
d83da1b818 | ||
|
|
0de09cd794 | ||
|
|
48c201af19 | ||
|
|
b0300d3063 | ||
|
|
e0e34894a3 | ||
|
|
18e3a8911f | ||
|
|
c505b1c32c | ||
|
|
e22e434258 | ||
|
|
8b91f9ff72 | ||
|
|
fadcfbdfea | ||
|
|
3c4c294f9c | ||
|
|
27a7e538df | ||
|
|
cb0cadcc5f | ||
|
|
2e1035a29f | ||
|
|
21c7f3cd92 | ||
|
|
13b6a5f4b2 | ||
|
|
78e8082a6f | ||
|
|
1de91fab67 | ||
|
|
4ef5436559 | ||
|
|
f2a6a769b0 | ||
|
|
8a767bd2ad | ||
|
|
7df243b860 | ||
|
|
f35d91933b | ||
|
|
286769a0f3 | ||
|
|
1dd91ec85b | ||
|
|
6adeb8b95e | ||
|
|
3e0d42bf9e | ||
|
|
41e22dabda | ||
|
|
39e7ee315e | ||
|
|
3e032e6cd6 | ||
|
|
609f4af600 | ||
|
|
4c27e35445 | ||
|
|
b0c967c039 | ||
|
|
c51da00bfb | ||
|
|
f3389f5d8b | ||
|
|
3b1971be66 | ||
|
|
4cb518930c | ||
|
|
c835bff570 | ||
|
|
6ee02fc62d | ||
|
|
8095f922bc | ||
|
|
77e5f3733e | ||
|
|
c47687dd21 | ||
|
|
8662433142 | ||
|
|
5f073c2a76 | ||
|
|
c5d67dd97a | ||
|
|
9b421450b1 | ||
|
|
a6740e7be3 | ||
|
|
65ef35f1b4 | ||
|
|
520253e762 | ||
|
|
aa3614a32b | ||
|
|
94492c45cb | ||
|
|
8f261bb27c | ||
|
|
ddd08342c8 | ||
|
|
c7db213ee9 | ||
|
|
220248dd3d | ||
|
|
5932160f15 | ||
|
|
76e0619b79 | ||
|
|
646a52a2e7 | ||
|
|
e1322df8b0 | ||
|
|
092a9dc6bd | ||
|
|
9f71fe707f | ||
|
|
b8311a62e7 | ||
|
|
13830ff4cb | ||
|
|
c1b858b2cf | ||
|
|
a035ac579c | ||
|
|
20c10e33c4 | ||
|
|
a4e4ce1c72 | ||
|
|
983936af8c | ||
|
|
62dfeac441 | ||
|
|
b81e1a228a | ||
|
|
5899920e48 | ||
|
|
8dee460397 | ||
|
|
cda54e0bea | ||
|
|
0554bf4e2d | ||
|
|
b92803e77f | ||
|
|
69e83071ff | ||
|
|
875765e6dc | ||
|
|
db56e26df9 | ||
|
|
5a88641228 | ||
|
|
16559e7595 | ||
|
|
d594d5d4a7 | ||
|
|
e950a2fa58 | ||
|
|
1df38cb782 | ||
|
|
c6400b6673 | ||
|
|
dbf2325c01 | ||
|
|
dd5b25399a | ||
|
|
8178ee4e58 | ||
|
|
ad1b41ea81 | ||
|
|
efd8528db0 | ||
|
|
e54a15978f | ||
|
|
d78b9ded2d | ||
|
|
53e8130c9c | ||
|
|
55c70a5ba8 | ||
|
|
ebbdd7bfda | ||
|
|
863f161466 | ||
|
|
9305ecb3bc | ||
|
|
0002bb8e5a | ||
|
|
b42fb77451 | ||
|
|
5a8e166289 | ||
|
|
5fa719143c | ||
|
|
a906f139c3 | ||
|
|
56363ea7e7 | ||
|
|
01e1e1fe11 | ||
|
|
4477dc7a66 | ||
|
|
45994e344e | ||
|
|
51d5e1afae | ||
|
|
577b958c4d | ||
|
|
ce38d8ced6 | ||
|
|
d65fcf286d | ||
|
|
5a6a0b37d6 |
4
.github/pyright-config.json
vendored
4
.github/pyright-config.json
vendored
@@ -2,11 +2,15 @@
|
||||
"include": [
|
||||
"../BizHawkClient.py",
|
||||
"../Patch.py",
|
||||
"../rule_builder/cached_world.py",
|
||||
"../rule_builder/options.py",
|
||||
"../rule_builder/rules.py",
|
||||
"../test/param.py",
|
||||
"../test/general/test_groups.py",
|
||||
"../test/general/test_helpers.py",
|
||||
"../test/general/test_memory.py",
|
||||
"../test/general/test_names.py",
|
||||
"../test/general/test_rule_builder.py",
|
||||
"../test/multiworld/__init__.py",
|
||||
"../test/multiworld/test_multiworlds.py",
|
||||
"../test/netutils/__init__.py",
|
||||
|
||||
3
.github/workflows/build.yml
vendored
3
.github/workflows/build.yml
vendored
@@ -1,4 +1,5 @@
|
||||
# This workflow will build a release-like distribution when manually dispatched
|
||||
# This workflow will build a release-like distribution when manually dispatched:
|
||||
# a Windows x64 7zip, a Windows x64 Installer, a Linux AppImage and a Linux binary .tar.gz.
|
||||
|
||||
name: Build
|
||||
|
||||
|
||||
143
.github/workflows/docker.yml
vendored
143
.github/workflows/docker.yml
vendored
@@ -11,144 +11,43 @@ on:
|
||||
- "!.github/workflows/**"
|
||||
- ".github/workflows/docker.yml"
|
||||
branches:
|
||||
- "main"
|
||||
- "dock-dev"
|
||||
tags:
|
||||
- "v?[0-9]+.[0-9]+.[0-9]*"
|
||||
workflow_dispatch:
|
||||
|
||||
env:
|
||||
REGISTRY: ghcr.io
|
||||
|
||||
jobs:
|
||||
prepare:
|
||||
push_to_registry:
|
||||
name: Push Docker image to Docker Hub
|
||||
runs-on: ubuntu-latest
|
||||
outputs:
|
||||
image-name: ${{ steps.image.outputs.name }}
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
package-name: ${{ steps.package.outputs.name }}
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
|
||||
- name: Set lowercase image name
|
||||
id: image
|
||||
run: |
|
||||
echo "name=${GITHUB_REPOSITORY,,}" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Set package name
|
||||
id: package
|
||||
run: |
|
||||
echo "name=$(basename ${GITHUB_REPOSITORY,,})" >> $GITHUB_OUTPUT
|
||||
|
||||
- name: Extract metadata
|
||||
id: meta
|
||||
uses: docker/metadata-action@v5
|
||||
with:
|
||||
images: ${{ env.REGISTRY }}/${{ steps.image.outputs.name }}
|
||||
tags: |
|
||||
type=ref,event=branch,enable={{is_not_default_branch}}
|
||||
type=semver,pattern={{version}}
|
||||
type=semver,pattern={{major}}.{{minor}}
|
||||
type=raw,value=nightly,enable={{is_default_branch}}
|
||||
|
||||
- name: Compute final tags
|
||||
id: final-tags
|
||||
run: |
|
||||
readarray -t tags <<< "${{ steps.meta.outputs.tags }}"
|
||||
|
||||
if [[ "${{ github.ref_type }}" == "tag" ]]; then
|
||||
tag="${{ github.ref_name }}"
|
||||
if [[ "$tag" =~ ^v?[0-9]+\.[0-9]+\.[0-9]+$ ]]; then
|
||||
full_latest="${{ env.REGISTRY }}/${{ steps.image.outputs.name }}:latest"
|
||||
# Check if latest is already in tags to avoid duplicates
|
||||
if ! printf '%s\n' "${tags[@]}" | grep -q "^$full_latest$"; then
|
||||
tags+=("$full_latest")
|
||||
fi
|
||||
fi
|
||||
fi
|
||||
|
||||
# Set multiline output
|
||||
echo "tags<<EOF" >> $GITHUB_OUTPUT
|
||||
printf '%s\n' "${tags[@]}" >> $GITHUB_OUTPUT
|
||||
echo "EOF" >> $GITHUB_OUTPUT
|
||||
|
||||
build:
|
||||
needs: prepare
|
||||
runs-on: ${{ matrix.runner }}
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
strategy:
|
||||
matrix:
|
||||
include:
|
||||
- platform: amd64
|
||||
runner: ubuntu-latest
|
||||
suffix: amd64
|
||||
cache-scope: amd64
|
||||
- platform: arm64
|
||||
runner: ubuntu-24.04-arm
|
||||
suffix: arm64
|
||||
cache-scope: arm64
|
||||
contents: read
|
||||
attestations: write
|
||||
id-token: write
|
||||
steps:
|
||||
- name: Checkout repository
|
||||
uses: actions/checkout@v4
|
||||
- name: Check out the repo
|
||||
uses: actions/checkout@v5
|
||||
|
||||
- name: Set up Docker Buildx
|
||||
uses: docker/setup-buildx-action@v3
|
||||
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
- name: Log in to Docker Hub
|
||||
uses: docker/login-action@f4ef78c080cd8ba55a85445d5b36e214a81df20a
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
username: ${{ secrets.DOCKERHUB_USER }}
|
||||
password: ${{ secrets.DOCKERHUB_TOKEN }}
|
||||
|
||||
- name: Compute suffixed tags
|
||||
id: tags
|
||||
run: |
|
||||
readarray -t tags <<< "${{ needs.prepare.outputs.tags }}"
|
||||
suffixed=()
|
||||
for t in "${tags[@]}"; do
|
||||
suffixed+=("$t-${{ matrix.suffix }}")
|
||||
done
|
||||
echo "tags=$(IFS=','; echo "${suffixed[*]}")" >> $GITHUB_OUTPUT
|
||||
- name: Extract metadata (tags, labels) for Docker
|
||||
id: meta
|
||||
uses: docker/metadata-action@9ec57ed1fcdbf14dcef7dfbe97b2010124a938b7
|
||||
with:
|
||||
images: ubufugu/dockipelago
|
||||
|
||||
- name: Build and push Docker image
|
||||
uses: docker/build-push-action@v5
|
||||
id: push
|
||||
uses: docker/build-push-action@3b5e8027fcad23fda98b2e3ac259d8d67585f671
|
||||
with:
|
||||
context: .
|
||||
file: ./Dockerfile
|
||||
platforms: linux/${{ matrix.platform }}
|
||||
push: true
|
||||
tags: ${{ steps.tags.outputs.tags }}
|
||||
labels: ${{ needs.prepare.outputs.labels }}
|
||||
cache-from: type=gha,scope=${{ matrix.cache-scope }}
|
||||
cache-to: type=gha,mode=max,scope=${{ matrix.cache-scope }}
|
||||
provenance: false
|
||||
|
||||
manifest:
|
||||
needs: [prepare, build]
|
||||
runs-on: ubuntu-latest
|
||||
permissions:
|
||||
contents: read
|
||||
packages: write
|
||||
steps:
|
||||
- name: Log in to GitHub Container Registry
|
||||
uses: docker/login-action@v3
|
||||
with:
|
||||
registry: ${{ env.REGISTRY }}
|
||||
username: ${{ github.actor }}
|
||||
password: ${{ secrets.GITHUB_TOKEN }}
|
||||
|
||||
- name: Create and push multi-arch manifest
|
||||
run: |
|
||||
readarray -t tag_array <<< "${{ needs.prepare.outputs.tags }}"
|
||||
|
||||
for tag in "${tag_array[@]}"; do
|
||||
docker manifest create "$tag" \
|
||||
"$tag-amd64" \
|
||||
"$tag-arm64"
|
||||
|
||||
docker manifest push "$tag"
|
||||
done
|
||||
tags: ${{ steps.meta.outputs.tags }}
|
||||
labels: ${{ steps.meta.outputs.labels }}
|
||||
3
.gitignore
vendored
3
.gitignore
vendored
@@ -63,7 +63,10 @@ Output Logs/
|
||||
/installdelete.iss
|
||||
/data/user.kv
|
||||
/datapackage
|
||||
/datapackage_export.json
|
||||
/custom_worlds
|
||||
# stubgen output
|
||||
/out/
|
||||
|
||||
# Byte-compiled / optimized / DLL files
|
||||
__pycache__/
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
<component name="ProjectRunConfigurationManager">
|
||||
<configuration default="false" name="Build APWorld" type="PythonConfigurationType" factoryName="Python">
|
||||
<configuration default="false" name="Build APWorlds" type="PythonConfigurationType" factoryName="Python">
|
||||
<module name="Archipelago" />
|
||||
<option name="ENV_FILES" value="" />
|
||||
<option name="INTERPRETER_OPTIONS" value="" />
|
||||
@@ -8,10 +8,10 @@ import secrets
|
||||
import warnings
|
||||
from argparse import Namespace
|
||||
from collections import Counter, deque, defaultdict
|
||||
from collections.abc import Collection, MutableSequence
|
||||
from collections.abc import Callable, Collection, Iterable, Iterator, Mapping, MutableSequence, Set
|
||||
from enum import IntEnum, IntFlag
|
||||
from typing import (AbstractSet, Any, Callable, ClassVar, Dict, Iterable, Iterator, List, Literal, Mapping, NamedTuple,
|
||||
Optional, Protocol, Set, Tuple, Union, TYPE_CHECKING, Literal, overload)
|
||||
from typing import (AbstractSet, Any, ClassVar, Dict, List, Literal, NamedTuple,
|
||||
Optional, Protocol, Tuple, Union, TYPE_CHECKING, overload)
|
||||
import dataclasses
|
||||
|
||||
from typing_extensions import NotRequired, TypedDict
|
||||
@@ -22,6 +22,7 @@ import Utils
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from entrance_rando import ERPlacementState
|
||||
from rule_builder.rules import Rule
|
||||
from worlds import AutoWorld
|
||||
|
||||
|
||||
@@ -85,7 +86,7 @@ class MultiWorld():
|
||||
local_items: Dict[int, Options.LocalItems]
|
||||
non_local_items: Dict[int, Options.NonLocalItems]
|
||||
progression_balancing: Dict[int, Options.ProgressionBalancing]
|
||||
completion_condition: Dict[int, Callable[[CollectionState], bool]]
|
||||
completion_condition: Dict[int, CollectionRule]
|
||||
indirect_connections: Dict[Region, Set[Entrance]]
|
||||
exclude_locations: Dict[int, Options.ExcludeLocations]
|
||||
priority_locations: Dict[int, Options.PriorityLocations]
|
||||
@@ -766,7 +767,7 @@ class CollectionState():
|
||||
else:
|
||||
self._update_reachable_regions_auto_indirect_conditions(player, queue)
|
||||
|
||||
def _update_reachable_regions_explicit_indirect_conditions(self, player: int, queue: deque):
|
||||
def _update_reachable_regions_explicit_indirect_conditions(self, player: int, queue: deque[Entrance]):
|
||||
reachable_regions = self.reachable_regions[player]
|
||||
blocked_connections = self.blocked_connections[player]
|
||||
# run BFS on all connections, and keep track of those blocked by missing items
|
||||
@@ -784,13 +785,16 @@ class CollectionState():
|
||||
blocked_connections.update(new_region.exits)
|
||||
queue.extend(new_region.exits)
|
||||
self.path[new_region] = (new_region.name, self.path.get(connection, None))
|
||||
self.multiworld.worlds[player].reached_region(self, new_region)
|
||||
|
||||
# Retry connections if the new region can unblock them
|
||||
for new_entrance in self.multiworld.indirect_connections.get(new_region, set()):
|
||||
if new_entrance in blocked_connections and new_entrance not in queue:
|
||||
queue.append(new_entrance)
|
||||
entrances = self.multiworld.indirect_connections.get(new_region)
|
||||
if entrances is not None:
|
||||
relevant_entrances = entrances.intersection(blocked_connections)
|
||||
relevant_entrances.difference_update(queue)
|
||||
queue.extend(relevant_entrances)
|
||||
|
||||
def _update_reachable_regions_auto_indirect_conditions(self, player: int, queue: deque):
|
||||
def _update_reachable_regions_auto_indirect_conditions(self, player: int, queue: deque[Entrance]):
|
||||
reachable_regions = self.reachable_regions[player]
|
||||
blocked_connections = self.blocked_connections[player]
|
||||
new_connection: bool = True
|
||||
@@ -812,6 +816,7 @@ class CollectionState():
|
||||
queue.extend(new_region.exits)
|
||||
self.path[new_region] = (new_region.name, self.path.get(connection, None))
|
||||
new_connection = True
|
||||
self.multiworld.worlds[player].reached_region(self, new_region)
|
||||
# sweep for indirect connections, mostly Entrance.can_reach(unrelated_Region)
|
||||
queue.extend(blocked_connections)
|
||||
|
||||
@@ -1169,13 +1174,17 @@ class CollectionState():
|
||||
self.prog_items[player][item] = count
|
||||
|
||||
|
||||
CollectionRule = Callable[[CollectionState], bool]
|
||||
DEFAULT_COLLECTION_RULE: CollectionRule = staticmethod(lambda state: True)
|
||||
|
||||
|
||||
class EntranceType(IntEnum):
|
||||
ONE_WAY = 1
|
||||
TWO_WAY = 2
|
||||
|
||||
|
||||
class Entrance:
|
||||
access_rule: Callable[[CollectionState], bool] = staticmethod(lambda state: True)
|
||||
access_rule: CollectionRule = DEFAULT_COLLECTION_RULE
|
||||
hide_path: bool = False
|
||||
player: int
|
||||
name: str
|
||||
@@ -1362,7 +1371,7 @@ class Region:
|
||||
self,
|
||||
location_name: str,
|
||||
item_name: str | None = None,
|
||||
rule: Callable[[CollectionState], bool] | None = None,
|
||||
rule: CollectionRule | Rule[Any] | None = None,
|
||||
location_type: type[Location] | None = None,
|
||||
item_type: type[Item] | None = None,
|
||||
show_in_spoiler: bool = True,
|
||||
@@ -1390,7 +1399,7 @@ class Region:
|
||||
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
|
||||
self.multiworld.worlds[self.player].set_rule(event_location, rule)
|
||||
|
||||
event_item = item_type(item_name, ItemClassification.progression, None, self.player)
|
||||
|
||||
@@ -1401,7 +1410,7 @@ class Region:
|
||||
return event_item
|
||||
|
||||
def connect(self, connecting_region: Region, name: Optional[str] = None,
|
||||
rule: Optional[Callable[[CollectionState], bool]] = None) -> Entrance:
|
||||
rule: Optional[CollectionRule | Rule[Any]] = None) -> Entrance:
|
||||
"""
|
||||
Connects this Region to another Region, placing the provided rule on the connection.
|
||||
|
||||
@@ -1409,8 +1418,8 @@ class Region:
|
||||
:param name: name of the connection being created
|
||||
:param rule: callable to determine access of this connection to go from self to the exiting_region"""
|
||||
exit_ = self.create_exit(name if name else f"{self.name} -> {connecting_region.name}")
|
||||
if rule:
|
||||
exit_.access_rule = rule
|
||||
if rule is not None:
|
||||
self.multiworld.worlds[self.player].set_rule(exit_, rule)
|
||||
exit_.connect(connecting_region)
|
||||
return exit_
|
||||
|
||||
@@ -1435,7 +1444,7 @@ class Region:
|
||||
return entrance
|
||||
|
||||
def add_exits(self, exits: Iterable[str] | Mapping[str, str | None],
|
||||
rules: Mapping[str, Callable[[CollectionState], bool]] | None = None) -> List[Entrance]:
|
||||
rules: Mapping[str, CollectionRule | Rule[Any]] | None = None) -> List[Entrance]:
|
||||
"""
|
||||
Connects current region to regions in exit dictionary. Passed region names must exist first.
|
||||
|
||||
@@ -1474,7 +1483,7 @@ class Location:
|
||||
show_in_spoiler: bool = True
|
||||
progress_type: LocationProgressType = LocationProgressType.DEFAULT
|
||||
always_allow: Callable[[CollectionState, Item], bool] = staticmethod(lambda state, item: False)
|
||||
access_rule: Callable[[CollectionState], bool] = staticmethod(lambda state: True)
|
||||
access_rule: CollectionRule = DEFAULT_COLLECTION_RULE
|
||||
item_rule: Callable[[Item], bool] = staticmethod(lambda item: True)
|
||||
item: Optional[Item] = None
|
||||
|
||||
@@ -1551,7 +1560,7 @@ class ItemClassification(IntFlag):
|
||||
skip_balancing = 0b01000
|
||||
""" should technically never occur on its own
|
||||
Item that is logically relevant, but progression balancing should not touch.
|
||||
|
||||
|
||||
Possible reasons for why an item should not be pulled ahead by progression balancing:
|
||||
1. This item is quite insignificant, so pulling it earlier doesn't help (currency/etc.)
|
||||
2. It is important for the player experience that this item is evenly distributed in the seed (e.g. goal items) """
|
||||
@@ -1559,13 +1568,13 @@ class ItemClassification(IntFlag):
|
||||
deprioritized = 0b10000
|
||||
""" Should technically never occur on its own.
|
||||
Will not be considered for priority locations,
|
||||
unless Priority Locations Fill runs out of regular progression items before filling all priority locations.
|
||||
|
||||
unless Priority Locations Fill runs out of regular progression items before filling all priority locations.
|
||||
|
||||
Should be used for items that would feel bad for the player to find on a priority location.
|
||||
Usually, these are items that are plentiful or insignificant. """
|
||||
|
||||
progression_deprioritized_skip_balancing = 0b11001
|
||||
""" Since a common case of both skip_balancing and deprioritized is "insignificant progression",
|
||||
""" Since a common case of both skip_balancing and deprioritized is "insignificant progression",
|
||||
these items often want both flags. """
|
||||
|
||||
progression_skip_balancing = 0b01001 # only progression gets balanced
|
||||
|
||||
@@ -24,7 +24,7 @@ if __name__ == "__main__":
|
||||
from MultiServer import CommandProcessor, mark_raw
|
||||
from NetUtils import (Endpoint, decode, NetworkItem, encode, JSONtoTextParser, ClientStatus, Permission, NetworkSlot,
|
||||
RawJSONtoTextParser, add_json_text, add_json_location, add_json_item, JSONTypes, HintStatus, SlotType)
|
||||
from Utils import Version, stream_input, async_start
|
||||
from Utils import gui_enabled, Version, stream_input, async_start
|
||||
from worlds import network_data_package, AutoWorldRegister
|
||||
import os
|
||||
import ssl
|
||||
@@ -35,9 +35,6 @@ if typing.TYPE_CHECKING:
|
||||
|
||||
logger = logging.getLogger("Client")
|
||||
|
||||
# without terminal, we have to use gui mode
|
||||
gui_enabled = not sys.stdout or "--nogui" not in sys.argv
|
||||
|
||||
|
||||
@Utils.cache_argsless
|
||||
def get_ssl_context():
|
||||
@@ -65,6 +62,8 @@ class ClientCommandProcessor(CommandProcessor):
|
||||
|
||||
def _cmd_exit(self) -> bool:
|
||||
"""Close connections and client"""
|
||||
if self.ctx.ui:
|
||||
self.ctx.ui.stop()
|
||||
self.ctx.exit_event.set()
|
||||
return True
|
||||
|
||||
|
||||
128
Generate.py
128
Generate.py
@@ -23,7 +23,7 @@ from BaseClasses import seeddigits, get_seed, PlandoOptions
|
||||
from Utils import parse_yamls, version_tuple, __version__, tuplize_version
|
||||
|
||||
|
||||
def mystery_argparse(argv: list[str] | None = None):
|
||||
def mystery_argparse(argv: list[str] | None = None) -> argparse.Namespace:
|
||||
from settings import get_settings
|
||||
settings = get_settings()
|
||||
defaults = settings.generator
|
||||
@@ -68,7 +68,7 @@ def mystery_argparse(argv: list[str] | None = None):
|
||||
args.weights_file_path = os.path.join(args.player_files_path, args.weights_file_path)
|
||||
if not os.path.isabs(args.meta_file_path):
|
||||
args.meta_file_path = os.path.join(args.player_files_path, args.meta_file_path)
|
||||
args.plando: PlandoOptions = PlandoOptions.from_option_string(args.plando)
|
||||
args.plando = PlandoOptions.from_option_string(args.plando)
|
||||
|
||||
return args
|
||||
|
||||
@@ -119,9 +119,9 @@ def main(args=None) -> tuple[argparse.Namespace, int]:
|
||||
else:
|
||||
meta_weights = None
|
||||
|
||||
|
||||
player_id = 1
|
||||
player_files = {}
|
||||
player_id: int = 1
|
||||
player_files: dict[int, str] = {}
|
||||
player_errors: list[str] = []
|
||||
for file in os.scandir(args.player_files_path):
|
||||
fname = file.name
|
||||
if file.is_file() and not fname.startswith(".") and not fname.lower().endswith(".ini") and \
|
||||
@@ -135,9 +135,13 @@ def main(args=None) -> tuple[argparse.Namespace, int]:
|
||||
else:
|
||||
weights_for_file.append(yaml)
|
||||
weights_cache[fname] = tuple(weights_for_file)
|
||||
|
||||
|
||||
except Exception as e:
|
||||
raise ValueError(f"File {fname} is invalid. Please fix your yaml.") from e
|
||||
logging.exception(f"Exception reading weights in file {fname}")
|
||||
player_errors.append(
|
||||
f"{len(player_errors) + 1}. "
|
||||
f"File {fname} is invalid. Please fix your yaml.\n{Utils.get_all_causes(e)}"
|
||||
)
|
||||
|
||||
# sort dict for consistent results across platforms:
|
||||
weights_cache = {key: value for key, value in sorted(weights_cache.items(), key=lambda k: k[0].casefold())}
|
||||
@@ -152,6 +156,10 @@ def main(args=None) -> tuple[argparse.Namespace, int]:
|
||||
args.multi = max(player_id - 1, args.multi)
|
||||
|
||||
if args.multi == 0:
|
||||
if player_errors:
|
||||
errors = "\n\n".join(player_errors)
|
||||
raise ValueError(f"Encountered {len(player_errors)} error(s) in player files. "
|
||||
f"See logs for full tracebacks.\n\n{errors}")
|
||||
raise ValueError(
|
||||
"No individual player files found and number of players is 0. "
|
||||
"Provide individual player files or specify the number of players via host.yaml or --multi."
|
||||
@@ -161,6 +169,10 @@ def main(args=None) -> tuple[argparse.Namespace, int]:
|
||||
f"{seed_name} Seed {seed} with plando: {args.plando}")
|
||||
|
||||
if not weights_cache:
|
||||
if player_errors:
|
||||
errors = "\n\n".join(player_errors)
|
||||
raise ValueError(f"Encountered {len(player_errors)} error(s) in player files. "
|
||||
f"See logs for full tracebacks.\n\n{errors}")
|
||||
raise Exception(f"No weights found. "
|
||||
f"Provide a general weights file ({args.weights_file_path}) or individual player files. "
|
||||
f"A mix is also permitted.")
|
||||
@@ -171,10 +183,6 @@ def main(args=None) -> tuple[argparse.Namespace, int]:
|
||||
args.sprite_pool = dict.fromkeys(range(1, args.multi+1), None)
|
||||
args.name = {}
|
||||
|
||||
settings_cache: dict[str, tuple[argparse.Namespace, ...]] = \
|
||||
{fname: (tuple(roll_settings(yaml, args.plando) for yaml in yamls) if args.sameoptions else None)
|
||||
for fname, yamls in weights_cache.items()}
|
||||
|
||||
if meta_weights:
|
||||
for category_name, category_dict in meta_weights.items():
|
||||
for key in category_dict:
|
||||
@@ -197,47 +205,85 @@ def main(args=None) -> tuple[argparse.Namespace, int]:
|
||||
else:
|
||||
yaml[category_name][key] = option
|
||||
|
||||
player_path_cache = {}
|
||||
settings_cache: dict[str, tuple[argparse.Namespace, ...] | None] = {fname: None for fname in weights_cache}
|
||||
if args.sameoptions:
|
||||
for fname, yamls in weights_cache.items():
|
||||
try:
|
||||
settings_cache[fname] = tuple(roll_settings(yaml, args.plando) for yaml in yamls)
|
||||
except Exception as e:
|
||||
logging.exception(f"Exception reading settings in file {fname}")
|
||||
player_errors.append(
|
||||
f"{len(player_errors) + 1}. "
|
||||
f"File {fname} is invalid. Please fix your yaml.\n{Utils.get_all_causes(e)}"
|
||||
)
|
||||
# Exit early here to avoid throwing the same errors again later
|
||||
if player_errors:
|
||||
errors = "\n\n".join(player_errors)
|
||||
raise ValueError(f"Encountered {len(player_errors)} error(s) in player files. "
|
||||
f"See logs for full tracebacks.\n\n{errors}")
|
||||
|
||||
player_path_cache: dict[int, str] = {}
|
||||
for player in range(1, args.multi + 1):
|
||||
player_path_cache[player] = player_files.get(player, args.weights_file_path)
|
||||
name_counter = Counter()
|
||||
name_counter: Counter[str] = Counter()
|
||||
args.player_options = {}
|
||||
|
||||
player = 1
|
||||
while player <= args.multi:
|
||||
path = player_path_cache[player]
|
||||
if path:
|
||||
if not path:
|
||||
player_errors.append(f'No weights specified for player {player}')
|
||||
player += 1
|
||||
continue
|
||||
|
||||
for doc_index, yaml in enumerate(weights_cache[path]):
|
||||
name = yaml.get("name")
|
||||
try:
|
||||
settings: tuple[argparse.Namespace, ...] = settings_cache[path] if settings_cache[path] else \
|
||||
tuple(roll_settings(yaml, args.plando) for yaml in weights_cache[path])
|
||||
for settingsObject in settings:
|
||||
for k, v in vars(settingsObject).items():
|
||||
if v is not None:
|
||||
try:
|
||||
getattr(args, k)[player] = v
|
||||
except AttributeError:
|
||||
setattr(args, k, {player: v})
|
||||
except Exception as e:
|
||||
raise Exception(f"Error setting {k} to {v} for player {player}") from e
|
||||
# Use the cached settings object if it exists, otherwise roll settings within the try-catch
|
||||
# Invariant: settings_cache[path] and weights_cache[path] have the same length
|
||||
cached = settings_cache[path]
|
||||
settings_object: argparse.Namespace = (cached[doc_index] if cached else roll_settings(yaml, args.plando))
|
||||
|
||||
# name was not specified
|
||||
if player not in args.name:
|
||||
if path == args.weights_file_path:
|
||||
# weights file, so we need to make the name unique
|
||||
args.name[player] = f"Player{player}"
|
||||
else:
|
||||
# use the filename
|
||||
args.name[player] = os.path.splitext(os.path.split(path)[-1])[0]
|
||||
args.name[player] = handle_name(args.name[player], player, name_counter)
|
||||
for k, v in vars(settings_object).items():
|
||||
if v is not None:
|
||||
try:
|
||||
getattr(args, k)[player] = v
|
||||
except AttributeError:
|
||||
setattr(args, k, {player: v})
|
||||
except Exception as e:
|
||||
raise Exception(f"Error setting {k} to {v} for player {player}") from e
|
||||
|
||||
# name was not specified
|
||||
if player not in args.name:
|
||||
if path == args.weights_file_path:
|
||||
# weights file, so we need to make the name unique
|
||||
args.name[player] = f"Player{player}"
|
||||
else:
|
||||
# use the filename
|
||||
args.name[player] = os.path.splitext(os.path.split(path)[-1])[0]
|
||||
args.name[player] = handle_name(args.name[player], player, name_counter)
|
||||
|
||||
player += 1
|
||||
except Exception as e:
|
||||
raise ValueError(f"File {path} is invalid. Please fix your yaml.") from e
|
||||
else:
|
||||
raise RuntimeError(f'No weights specified for player {player}')
|
||||
logging.exception(f"Exception reading settings in file {path} document #{doc_index + 1} "
|
||||
f"(name: {args.name.get(player, name)})")
|
||||
player_errors.append(
|
||||
f"{len(player_errors) + 1}. "
|
||||
f"File {path} document #{doc_index + 1} (name: {args.name.get(player, name)}) is invalid. "
|
||||
f"Please fix your yaml.\n{Utils.get_all_causes(e)}")
|
||||
|
||||
# increment for each yaml document in the file
|
||||
player += 1
|
||||
|
||||
if len(set(name.lower() for name in args.name.values())) != len(args.name):
|
||||
raise Exception(f"Names have to be unique. Names: {Counter(name.lower() for name in args.name.values())}")
|
||||
player_errors.append(
|
||||
f"{len(player_errors) + 1}. "
|
||||
f"Names have to be unique. Names: {Counter(name.lower() for name in args.name.values())}"
|
||||
)
|
||||
|
||||
if player_errors:
|
||||
errors = "\n\n".join(player_errors)
|
||||
raise ValueError(f"Encountered {len(player_errors)} error(s) in player files. "
|
||||
f"See logs for full tracebacks.\n\n{errors}")
|
||||
|
||||
return args, seed
|
||||
|
||||
@@ -316,7 +362,7 @@ class SafeFormatter(string.Formatter):
|
||||
return kwargs.get(key, "{" + key + "}")
|
||||
|
||||
|
||||
def handle_name(name: str, player: int, name_counter: Counter):
|
||||
def handle_name(name: str, player: int, name_counter: Counter[str]):
|
||||
name_counter[name.lower()] += 1
|
||||
number = name_counter[name.lower()]
|
||||
new_name = "%".join([x.replace("%number%", "{number}").replace("%player%", "{player}") for x in name.split("%%")])
|
||||
@@ -454,7 +500,7 @@ def roll_triggers(weights: dict, triggers: list, valid_keys: set) -> dict:
|
||||
return weights
|
||||
|
||||
|
||||
def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str, option: type(Options.Option), plando_options: PlandoOptions):
|
||||
def handle_option(ret: argparse.Namespace, game_weights: dict, option_key: str, option: type[Options.Option], plando_options: PlandoOptions):
|
||||
try:
|
||||
if option_key in game_weights:
|
||||
if not option.supports_weighting:
|
||||
|
||||
16
Launcher.py
16
Launcher.py
@@ -31,6 +31,10 @@ import settings
|
||||
import Utils
|
||||
from Utils import (init_logging, is_frozen, is_linux, is_macos, is_windows, local_path, messagebox, open_filename,
|
||||
user_path)
|
||||
|
||||
if __name__ == "__main__":
|
||||
init_logging('Launcher')
|
||||
|
||||
from worlds.LauncherComponents import Component, components, icon_paths, SuffixIdentifier, Type
|
||||
|
||||
|
||||
@@ -218,12 +222,17 @@ def launch(exe, in_terminal=False):
|
||||
|
||||
def create_shortcut(button: Any, component: Component) -> None:
|
||||
from pyshortcuts import make_shortcut
|
||||
script = sys.argv[0]
|
||||
wkdir = Utils.local_path()
|
||||
env = os.environ
|
||||
if "APPIMAGE" in env:
|
||||
script = env["ARGV0"]
|
||||
wkdir = None # defaults to ~ on Linux
|
||||
else:
|
||||
script = sys.argv[0]
|
||||
wkdir = Utils.local_path()
|
||||
|
||||
script = f"{script} \"{component.display_name}\""
|
||||
make_shortcut(script, name=f"Archipelago {component.display_name}", icon=local_path("data", "icon.ico"),
|
||||
startmenu=False, terminal=False, working_dir=wkdir)
|
||||
startmenu=False, terminal=False, working_dir=wkdir, noexe=Utils.is_frozen())
|
||||
button.menu.dismiss()
|
||||
|
||||
|
||||
@@ -488,7 +497,6 @@ def main(args: argparse.Namespace | dict | None = None):
|
||||
|
||||
|
||||
if __name__ == '__main__':
|
||||
init_logging('Launcher')
|
||||
multiprocessing.freeze_support()
|
||||
multiprocessing.set_start_method("spawn") # if launched process uses kivy, fork won't work
|
||||
parser = argparse.ArgumentParser(
|
||||
|
||||
@@ -5,15 +5,16 @@ import multiprocessing
|
||||
import warnings
|
||||
|
||||
|
||||
if sys.platform in ("win32", "darwin") and sys.version_info < (3, 11, 9):
|
||||
if sys.platform in ("win32", "darwin") and not (3, 11, 9) <= sys.version_info < (3, 14, 0):
|
||||
# Official micro version updates. This should match the number in docs/running from source.md.
|
||||
raise RuntimeError(f"Incompatible Python Version found: {sys.version_info}. Official 3.11.9+ is supported.")
|
||||
raise RuntimeError(f"Incompatible Python Version found: {sys.version_info}. "
|
||||
"Official 3.11.9 through 3.13.x is supported.")
|
||||
elif sys.platform in ("win32", "darwin") and sys.version_info < (3, 11, 13):
|
||||
# There are known security issues, but no easy way to install fixed versions on Windows for testing.
|
||||
warnings.warn(f"Python Version {sys.version_info} has security issues. Don't use in production.")
|
||||
elif sys.version_info < (3, 11, 0):
|
||||
elif not (3, 11, 0) <= sys.version_info < (3, 14, 0):
|
||||
# Other platforms may get security backports instead of micro updates, so the number is unreliable.
|
||||
raise RuntimeError(f"Incompatible Python Version found: {sys.version_info}. 3.11.0+ is supported.")
|
||||
raise RuntimeError(f"Incompatible Python Version found: {sys.version_info}. 3.11.0 through 3.13.x is supported.")
|
||||
|
||||
# don't run update if environment is frozen/compiled or if not the parent process (skip in subprocess)
|
||||
_skip_update = bool(
|
||||
|
||||
@@ -69,6 +69,12 @@ def remove_from_list(container, value):
|
||||
|
||||
|
||||
def pop_from_container(container, value):
|
||||
if isinstance(container, list) and isinstance(value, int) and len(container) <= value:
|
||||
return container
|
||||
|
||||
if isinstance(container, dict) and value not in container:
|
||||
return container
|
||||
|
||||
try:
|
||||
container.pop(value)
|
||||
except ValueError:
|
||||
@@ -911,12 +917,6 @@ async def server(websocket: "ServerConnection", path: str = "/", ctx: Context =
|
||||
|
||||
|
||||
async def on_client_connected(ctx: Context, client: Client):
|
||||
players = []
|
||||
for team, clients in ctx.clients.items():
|
||||
for slot, connected_clients in clients.items():
|
||||
if connected_clients:
|
||||
name = ctx.player_names[team, slot]
|
||||
players.append(NetworkPlayer(team, slot, ctx.name_aliases.get((team, slot), name), name))
|
||||
games = {ctx.games[x] for x in range(1, len(ctx.games) + 1)}
|
||||
games.add("Archipelago")
|
||||
await ctx.send_msgs(client, [{
|
||||
@@ -1364,7 +1364,10 @@ class CommandProcessor(metaclass=CommandMeta):
|
||||
argname += "=" + parameter.default
|
||||
argtext += argname
|
||||
argtext += " "
|
||||
doctext = '\n '.join(inspect.getdoc(method).split('\n'))
|
||||
method_doc = inspect.getdoc(method)
|
||||
if method_doc is None:
|
||||
method_doc = "(missing help text)"
|
||||
doctext = "\n ".join(method_doc.split("\n"))
|
||||
s += f"{self.marker}{command} {argtext}\n {doctext}\n"
|
||||
return s
|
||||
|
||||
|
||||
187
Options.py
187
Options.py
@@ -24,6 +24,39 @@ if typing.TYPE_CHECKING:
|
||||
import pathlib
|
||||
|
||||
|
||||
_RANDOM_OPTS = [
|
||||
"random", "random-low", "random-middle", "random-high",
|
||||
"random-range-low-<min>-<max>", "random-range-middle-<min>-<max>",
|
||||
"random-range-high-<min>-<max>", "random-range-<min>-<max>",
|
||||
]
|
||||
|
||||
|
||||
def triangular(lower: int, end: int, tri: float = 0.5) -> int:
|
||||
"""
|
||||
Integer triangular distribution for `lower` inclusive to `end` inclusive.
|
||||
|
||||
Expects `lower <= end` and `0.0 <= tri <= 1.0`. The result of other inputs is undefined.
|
||||
"""
|
||||
# Use the continuous range [lower, end + 1) to produce an integer result in [lower, end].
|
||||
# random.triangular is actually [a, b] and not [a, b), so there is a very small chance of getting exactly b even
|
||||
# when a != b, so ensure the result is never more than `end`.
|
||||
return min(end, math.floor(random.triangular(0.0, 1.0, tri) * (end - lower + 1) + lower))
|
||||
|
||||
|
||||
def random_weighted_range(text: str, range_start: int, range_end: int):
|
||||
if text == "random-low":
|
||||
return triangular(range_start, range_end, 0.0)
|
||||
elif text == "random-high":
|
||||
return triangular(range_start, range_end, 1.0)
|
||||
elif text == "random-middle":
|
||||
return triangular(range_start, range_end)
|
||||
elif text == "random":
|
||||
return random.randint(range_start, range_end)
|
||||
else:
|
||||
raise Exception(f"random text \"{text}\" did not resolve to a recognized pattern. "
|
||||
f"Acceptable values are: {', '.join(_RANDOM_OPTS)}.")
|
||||
|
||||
|
||||
def roll_percentage(percentage: int | float) -> bool:
|
||||
"""Roll a percentage chance.
|
||||
percentage is expected to be in range [0, 100]"""
|
||||
@@ -417,10 +450,12 @@ class Toggle(NumericOption):
|
||||
def from_text(cls, text: str) -> Toggle:
|
||||
if text == "random":
|
||||
return cls(random.choice(list(cls.name_lookup)))
|
||||
elif text.lower() in {"off", "0", "false", "none", "null", "no"}:
|
||||
elif text.lower() in {"off", "0", "false", "none", "null", "no", "disabled"}:
|
||||
return cls(0)
|
||||
else:
|
||||
elif text.lower() in {"on", "1", "true", "yes", "enabled"}:
|
||||
return cls(1)
|
||||
else:
|
||||
raise OptionError(f"Option {cls.__name__} does not support a value of {text}")
|
||||
|
||||
@classmethod
|
||||
def from_any(cls, data: typing.Any):
|
||||
@@ -523,9 +558,9 @@ class Choice(NumericOption):
|
||||
|
||||
class TextChoice(Choice):
|
||||
"""Allows custom string input and offers choices. Choices will resolve to int and text will resolve to string"""
|
||||
value: typing.Union[str, int]
|
||||
value: str | int
|
||||
|
||||
def __init__(self, value: typing.Union[str, int]):
|
||||
def __init__(self, value: str | int):
|
||||
assert isinstance(value, str) or isinstance(value, int), \
|
||||
f"'{value}' is not a valid option for '{self.__class__.__name__}'"
|
||||
self.value = value
|
||||
@@ -546,7 +581,7 @@ class TextChoice(Choice):
|
||||
return cls(text)
|
||||
|
||||
@classmethod
|
||||
def get_option_name(cls, value: T) -> str:
|
||||
def get_option_name(cls, value: str | int) -> str:
|
||||
if isinstance(value, str):
|
||||
return value
|
||||
return super().get_option_name(value)
|
||||
@@ -688,12 +723,6 @@ class Range(NumericOption):
|
||||
range_start = 0
|
||||
range_end = 1
|
||||
|
||||
_RANDOM_OPTS = [
|
||||
"random", "random-low", "random-middle", "random-high",
|
||||
"random-range-low-<min>-<max>", "random-range-middle-<min>-<max>",
|
||||
"random-range-high-<min>-<max>", "random-range-<min>-<max>",
|
||||
]
|
||||
|
||||
def __init__(self, value: int):
|
||||
if value < self.range_start:
|
||||
raise Exception(f"{value} is lower than minimum {self.range_start} for option {self.__class__.__name__}")
|
||||
@@ -742,25 +771,16 @@ class Range(NumericOption):
|
||||
|
||||
@classmethod
|
||||
def weighted_range(cls, text) -> Range:
|
||||
if text == "random-low":
|
||||
return cls(cls.triangular(cls.range_start, cls.range_end, 0.0))
|
||||
elif text == "random-high":
|
||||
return cls(cls.triangular(cls.range_start, cls.range_end, 1.0))
|
||||
elif text == "random-middle":
|
||||
return cls(cls.triangular(cls.range_start, cls.range_end))
|
||||
elif text.startswith("random-range-"):
|
||||
if text.startswith("random-range-"):
|
||||
return cls.custom_range(text)
|
||||
elif text == "random":
|
||||
return cls(random.randint(cls.range_start, cls.range_end))
|
||||
else:
|
||||
raise Exception(f"random text \"{text}\" did not resolve to a recognized pattern. "
|
||||
f"Acceptable values are: {', '.join(cls._RANDOM_OPTS)}.")
|
||||
return cls(random_weighted_range(text, cls.range_start, cls.range_end))
|
||||
|
||||
@classmethod
|
||||
def custom_range(cls, text) -> Range:
|
||||
textsplit = text.split("-")
|
||||
try:
|
||||
random_range = [int(textsplit[len(textsplit) - 2]), int(textsplit[len(textsplit) - 1])]
|
||||
random_range = [int(textsplit[-2]), int(textsplit[-1])]
|
||||
except ValueError:
|
||||
raise ValueError(f"Invalid random range {text} for option {cls.__name__}")
|
||||
random_range.sort()
|
||||
@@ -768,14 +788,9 @@ class Range(NumericOption):
|
||||
raise Exception(
|
||||
f"{random_range[0]}-{random_range[1]} is outside allowed range "
|
||||
f"{cls.range_start}-{cls.range_end} for option {cls.__name__}")
|
||||
if text.startswith("random-range-low"):
|
||||
return cls(cls.triangular(random_range[0], random_range[1], 0.0))
|
||||
elif text.startswith("random-range-middle"):
|
||||
return cls(cls.triangular(random_range[0], random_range[1]))
|
||||
elif text.startswith("random-range-high"):
|
||||
return cls(cls.triangular(random_range[0], random_range[1], 1.0))
|
||||
else:
|
||||
return cls(random.randint(random_range[0], random_range[1]))
|
||||
if textsplit[2] in ("low", "middle", "high"):
|
||||
return cls(random_weighted_range(f"{textsplit[0]}-{textsplit[2]}", *random_range))
|
||||
return cls(random_weighted_range("random", *random_range))
|
||||
|
||||
@classmethod
|
||||
def from_any(cls, data: typing.Any) -> Range:
|
||||
@@ -790,18 +805,6 @@ class Range(NumericOption):
|
||||
def __str__(self) -> str:
|
||||
return str(self.value)
|
||||
|
||||
@staticmethod
|
||||
def triangular(lower: int, end: int, tri: float = 0.5) -> int:
|
||||
"""
|
||||
Integer triangular distribution for `lower` inclusive to `end` inclusive.
|
||||
|
||||
Expects `lower <= end` and `0.0 <= tri <= 1.0`. The result of other inputs is undefined.
|
||||
"""
|
||||
# Use the continuous range [lower, end + 1) to produce an integer result in [lower, end].
|
||||
# random.triangular is actually [a, b] and not [a, b), so there is a very small chance of getting exactly b even
|
||||
# when a != b, so ensure the result is never more than `end`.
|
||||
return min(end, math.floor(random.triangular(0.0, 1.0, tri) * (end - lower + 1) + lower))
|
||||
|
||||
|
||||
class NamedRange(Range):
|
||||
special_range_names: typing.Dict[str, int] = {}
|
||||
@@ -891,7 +894,7 @@ class VerifyKeys(metaclass=FreezeValidKeys):
|
||||
def __iter__(self) -> typing.Iterator[typing.Any]:
|
||||
return self.value.__iter__()
|
||||
|
||||
|
||||
|
||||
class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys, typing.Mapping[str, typing.Any]):
|
||||
default = {}
|
||||
supports_weighting = False
|
||||
@@ -906,7 +909,8 @@ class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys, typing.Mappin
|
||||
else:
|
||||
raise NotImplementedError(f"Cannot Convert from non-dictionary, got {type(data)}")
|
||||
|
||||
def get_option_name(self, value):
|
||||
@classmethod
|
||||
def get_option_name(cls, value):
|
||||
return ", ".join(f"{key}: {v}" for key, v in value.items())
|
||||
|
||||
def __getitem__(self, item: str) -> typing.Any:
|
||||
@@ -986,7 +990,8 @@ class OptionList(Option[typing.List[typing.Any]], VerifyKeys):
|
||||
return cls(data)
|
||||
return cls.from_text(str(data))
|
||||
|
||||
def get_option_name(self, value):
|
||||
@classmethod
|
||||
def get_option_name(cls, value):
|
||||
return ", ".join(map(str, value))
|
||||
|
||||
def __contains__(self, item):
|
||||
@@ -996,13 +1001,19 @@ class OptionList(Option[typing.List[typing.Any]], VerifyKeys):
|
||||
class OptionSet(Option[typing.Set[str]], VerifyKeys):
|
||||
default = frozenset()
|
||||
supports_weighting = False
|
||||
random_str: str | None
|
||||
|
||||
def __init__(self, value: typing.Iterable[str]):
|
||||
def __init__(self, value: typing.Iterable[str], random_str: str | None = None):
|
||||
self.value = set(deepcopy(value))
|
||||
self.random_str = random_str
|
||||
super(OptionSet, self).__init__()
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, text: str):
|
||||
check_text = text.lower().split(",")
|
||||
if ((cls.valid_keys or cls.verify_item_name or cls.verify_location_name)
|
||||
and len(check_text) == 1 and check_text[0].startswith("random")):
|
||||
return cls((), check_text[0])
|
||||
return cls([option.strip() for option in text.split(",")])
|
||||
|
||||
@classmethod
|
||||
@@ -1011,7 +1022,37 @@ class OptionSet(Option[typing.Set[str]], VerifyKeys):
|
||||
return cls(data)
|
||||
return cls.from_text(str(data))
|
||||
|
||||
def get_option_name(self, value):
|
||||
def verify(self, world: typing.Type[World], player_name: str, plando_options: PlandoOptions) -> None:
|
||||
if self.random_str and not self.value:
|
||||
choice_list = sorted(self.valid_keys)
|
||||
if self.verify_item_name:
|
||||
choice_list.extend(sorted(world.item_names))
|
||||
if self.verify_location_name:
|
||||
choice_list.extend(sorted(world.location_names))
|
||||
if self.random_str.startswith("random-range-"):
|
||||
textsplit = self.random_str.split("-")
|
||||
try:
|
||||
random_range = [int(textsplit[-2]), int(textsplit[-1])]
|
||||
except ValueError:
|
||||
raise ValueError(f"Invalid random range {self.random_str} for option {self.__class__.__name__} "
|
||||
f"for player {player_name}")
|
||||
random_range.sort()
|
||||
if random_range[0] < 0 or random_range[1] > len(choice_list):
|
||||
raise Exception(
|
||||
f"{random_range[0]}-{random_range[1]} is outside allowed range "
|
||||
f"0-{len(choice_list)} for option {self.__class__.__name__} for player {player_name}")
|
||||
if textsplit[2] in ("low", "middle", "high"):
|
||||
choice_count = random_weighted_range(f"{textsplit[0]}-{textsplit[2]}",
|
||||
random_range[0], random_range[1])
|
||||
else:
|
||||
choice_count = random_weighted_range("random", random_range[0], random_range[1])
|
||||
else:
|
||||
choice_count = random_weighted_range(self.random_str, 0, len(choice_list))
|
||||
self.value = set(random.sample(choice_list, k=choice_count))
|
||||
super(Option, self).verify(world, player_name, plando_options)
|
||||
|
||||
@classmethod
|
||||
def get_option_name(cls, value):
|
||||
return ", ".join(sorted(value))
|
||||
|
||||
def __contains__(self, item):
|
||||
@@ -1656,7 +1697,7 @@ class PlandoItems(Option[typing.List[PlandoItem]]):
|
||||
def __len__(self) -> int:
|
||||
return len(self.value)
|
||||
|
||||
|
||||
|
||||
class Removed(FreeText):
|
||||
"""This Option has been Removed."""
|
||||
rich_text_doc = True
|
||||
@@ -1742,8 +1783,10 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge
|
||||
from Utils import local_path, __version__
|
||||
|
||||
full_path: str
|
||||
preset_folder = os.path.join(target_folder, "Presets")
|
||||
|
||||
os.makedirs(target_folder, exist_ok=True)
|
||||
os.makedirs(preset_folder, exist_ok=True)
|
||||
|
||||
# clean out old
|
||||
for file in os.listdir(target_folder):
|
||||
@@ -1751,11 +1794,16 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge
|
||||
if os.path.isfile(full_path) and full_path.endswith(".yaml"):
|
||||
os.unlink(full_path)
|
||||
|
||||
def dictify_range(option: Range):
|
||||
data = {option.default: 50}
|
||||
for file in os.listdir(preset_folder):
|
||||
full_path = os.path.join(preset_folder, file)
|
||||
if os.path.isfile(full_path) and full_path.endswith(".yaml"):
|
||||
os.unlink(full_path)
|
||||
|
||||
def dictify_range(option: Range, option_val: int | str):
|
||||
data = {option_val: 50}
|
||||
for sub_option in ["random", "random-low", "random-high",
|
||||
f"random-range-{option.range_start}-{option.range_end}"]:
|
||||
if sub_option != option.default:
|
||||
if sub_option != option_val:
|
||||
data[sub_option] = 0
|
||||
notes = {
|
||||
"random-low": "random value weighted towards lower values",
|
||||
@@ -1768,6 +1816,8 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge
|
||||
if number in data:
|
||||
data[name] = data[number]
|
||||
del data[number]
|
||||
elif name in data:
|
||||
pass
|
||||
else:
|
||||
data[name] = 0
|
||||
|
||||
@@ -1783,20 +1833,27 @@ def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], ge
|
||||
|
||||
for game_name, world in AutoWorldRegister.world_types.items():
|
||||
if not world.hidden or generate_hidden:
|
||||
presets = world.web.options_presets.copy()
|
||||
presets.update({"": {}})
|
||||
|
||||
option_groups = get_option_groups(world)
|
||||
|
||||
res = template.render(
|
||||
option_groups=option_groups,
|
||||
__version__=__version__,
|
||||
game=game_name,
|
||||
world_version=world.world_version.as_simple_string(),
|
||||
yaml_dump=yaml_dump_scalar,
|
||||
dictify_range=dictify_range,
|
||||
cleandoc=cleandoc,
|
||||
)
|
||||
|
||||
with open(os.path.join(target_folder, get_file_safe_name(game_name) + ".yaml"), "w", encoding="utf-8-sig") as f:
|
||||
f.write(res)
|
||||
for name, preset in presets.items():
|
||||
res = template.render(
|
||||
option_groups=option_groups,
|
||||
__version__=__version__,
|
||||
game=game_name,
|
||||
world_version=world.world_version.as_simple_string(),
|
||||
yaml_dump=yaml_dump_scalar,
|
||||
dictify_range=dictify_range,
|
||||
cleandoc=cleandoc,
|
||||
preset_name=name,
|
||||
preset=preset,
|
||||
)
|
||||
preset_name = f" - {name}" if name else ""
|
||||
with open(os.path.join(preset_folder if name else target_folder,
|
||||
get_file_safe_name(game_name + preset_name) + ".yaml"),
|
||||
"w", encoding="utf-8-sig") as f:
|
||||
f.write(res)
|
||||
|
||||
|
||||
def dump_player_options(multiworld: MultiWorld) -> None:
|
||||
|
||||
@@ -6,6 +6,7 @@ if __name__ == "__main__":
|
||||
|
||||
from kvui import (ThemedApp, ScrollBox, MainLayout, ContainerLayout, dp, Widget, MDBoxLayout, TooltipLabel, MDLabel,
|
||||
ToggleButton, MarkupDropdown, ResizableTextField)
|
||||
from kivy.clock import Clock
|
||||
from kivy.uix.behaviors.button import ButtonBehavior
|
||||
from kivymd.uix.behaviors import RotateBehavior
|
||||
from kivymd.uix.anchorlayout import MDAnchorLayout
|
||||
@@ -269,34 +270,53 @@ class OptionsCreator(ThemedApp):
|
||||
self.options = {}
|
||||
super().__init__()
|
||||
|
||||
def export_options(self, button: Widget):
|
||||
if 0 < len(self.name_input.text) < 17 and self.current_game:
|
||||
file_name = Utils.save_filename("Export Options File As...", [("YAML", ["*.yaml"])],
|
||||
@staticmethod
|
||||
def show_result_snack(text: str) -> None:
|
||||
MDSnackbar(MDSnackbarText(text=text), y=dp(24), pos_hint={"center_x": 0.5}, size_hint_x=0.5).open()
|
||||
|
||||
def on_export_result(self, text: str | None) -> None:
|
||||
self.container.disabled = False
|
||||
if text is not None:
|
||||
Clock.schedule_once(lambda _: self.show_result_snack(text), 0)
|
||||
|
||||
def export_options_background(self, options: dict[str, typing.Any]) -> None:
|
||||
try:
|
||||
file_name = Utils.save_filename("Export Options File As...", [("YAML", [".yaml"])],
|
||||
Utils.get_file_safe_name(f"{self.name_input.text}.yaml"))
|
||||
except Exception:
|
||||
self.on_export_result("Could not open dialog. Already open?")
|
||||
raise
|
||||
|
||||
if not file_name:
|
||||
self.on_export_result(None) # No file selected. No need to show a message for this.
|
||||
return
|
||||
|
||||
try:
|
||||
with open(file_name, 'w') as f:
|
||||
f.write(Utils.dump(options, sort_keys=False))
|
||||
f.close()
|
||||
self.on_export_result("File saved successfully.")
|
||||
except Exception:
|
||||
self.on_export_result("Could not save file.")
|
||||
raise
|
||||
|
||||
def export_options(self, button: Widget) -> None:
|
||||
if 0 < len(self.name_input.text) < 17 and self.current_game:
|
||||
import threading
|
||||
options = {
|
||||
"name": self.name_input.text,
|
||||
"description": f"YAML generated by Archipelago {Utils.__version__}.",
|
||||
"game": self.current_game,
|
||||
self.current_game: {k: check_random(v) for k, v in self.options.items()}
|
||||
}
|
||||
try:
|
||||
with open(file_name, 'w') as f:
|
||||
f.write(Utils.dump(options, sort_keys=False))
|
||||
f.close()
|
||||
MDSnackbar(MDSnackbarText(text="File saved successfully."), y=dp(24), pos_hint={"center_x": 0.5},
|
||||
size_hint_x=0.5).open()
|
||||
except FileNotFoundError:
|
||||
MDSnackbar(MDSnackbarText(text="Saving cancelled."), y=dp(24), pos_hint={"center_x": 0.5},
|
||||
size_hint_x=0.5).open()
|
||||
threading.Thread(target=self.export_options_background, args=(options,), daemon=True).start()
|
||||
self.container.disabled = True
|
||||
elif not self.name_input.text:
|
||||
MDSnackbar(MDSnackbarText(text="Name must not be empty."), y=dp(24), pos_hint={"center_x": 0.5},
|
||||
size_hint_x=0.5).open()
|
||||
self.show_result_snack("Name must not be empty.")
|
||||
elif not self.current_game:
|
||||
MDSnackbar(MDSnackbarText(text="You must select a game to play."), y=dp(24), pos_hint={"center_x": 0.5},
|
||||
size_hint_x=0.5).open()
|
||||
self.show_result_snack("You must select a game to play.")
|
||||
else:
|
||||
MDSnackbar(MDSnackbarText(text="Name cannot be longer than 16 characters."), y=dp(24),
|
||||
pos_hint={"center_x": 0.5}, size_hint_x=0.5).open()
|
||||
self.show_result_snack("Name cannot be longer than 16 characters.")
|
||||
|
||||
def create_range(self, option: typing.Type[Range], name: str):
|
||||
def update_text(range_box: VisualRange):
|
||||
@@ -509,8 +529,10 @@ class OptionsCreator(ThemedApp):
|
||||
self.options[name] = "random-" + str(self.options[name])
|
||||
else:
|
||||
self.options[name] = self.options[name].replace("random-", "")
|
||||
if self.options[name].isnumeric() or self.options[name] in ("True", "False"):
|
||||
self.options[name] = eval(self.options[name])
|
||||
if self.options[name].isnumeric():
|
||||
self.options[name] = int(self.options[name])
|
||||
elif self.options[name] in ("True", "False"):
|
||||
self.options[name] = self.options[name] == "True"
|
||||
|
||||
base_object = instance.parent.parent
|
||||
label_object = instance.parent
|
||||
@@ -632,7 +654,7 @@ class OptionsCreator(ThemedApp):
|
||||
self.create_options_panel(world_btn)
|
||||
|
||||
for world, cls in sorted(AutoWorldRegister.world_types.items(), key=lambda x: x[0]):
|
||||
if world == "Archipelago":
|
||||
if cls.hidden:
|
||||
continue
|
||||
world_text = MDButtonText(text=world, size_hint_y=None, width=dp(150),
|
||||
pos_hint={"x": 0.03, "center_y": 0.5})
|
||||
|
||||
@@ -83,6 +83,8 @@ Currently, the following games are supported:
|
||||
* Celeste (Open World)
|
||||
* Choo-Choo Charles
|
||||
* APQuest
|
||||
* Satisfactory
|
||||
* EarthBound
|
||||
|
||||
For setup and instructions check out our [tutorials page](https://archipelago.gg/tutorial/).
|
||||
Downloads can be found at [Releases](https://github.com/ArchipelagoMW/Archipelago/releases), including compiled
|
||||
|
||||
174
Utils.py
174
Utils.py
@@ -22,6 +22,7 @@ from settings import Settings, get_settings
|
||||
from time import sleep
|
||||
from typing import BinaryIO, Coroutine, Optional, Set, Dict, Any, Union, TypeGuard
|
||||
from yaml import load, load_all, dump
|
||||
from pathspec import PathSpec, GitIgnoreSpec
|
||||
|
||||
try:
|
||||
from yaml import CLoader as UnsafeLoader, CSafeLoader as SafeLoader, CDumper as Dumper
|
||||
@@ -48,7 +49,7 @@ class Version(typing.NamedTuple):
|
||||
return ".".join(str(item) for item in self)
|
||||
|
||||
|
||||
__version__ = "0.6.5"
|
||||
__version__ = "0.6.7"
|
||||
version_tuple = tuplize_version(__version__)
|
||||
|
||||
is_linux = sys.platform.startswith("linux")
|
||||
@@ -387,6 +388,14 @@ def store_data_package_for_checksum(game: str, data: typing.Dict[str, Any]) -> N
|
||||
logging.debug(f"Could not store data package: {e}")
|
||||
|
||||
|
||||
def read_apignore(filename: str | pathlib.Path) -> PathSpec | None:
|
||||
try:
|
||||
with open(filename) as ignore_file:
|
||||
return GitIgnoreSpec.from_lines(ignore_file)
|
||||
except FileNotFoundError:
|
||||
return None
|
||||
|
||||
|
||||
def get_default_adjuster_settings(game_name: str) -> Namespace:
|
||||
import LttPAdjuster
|
||||
adjuster_settings = Namespace()
|
||||
@@ -802,29 +811,32 @@ def open_filename(title: str, filetypes: typing.Iterable[typing.Tuple[str, typin
|
||||
except tkinter.TclError:
|
||||
return None # GUI not available. None is the same as a user clicking "cancel"
|
||||
root.withdraw()
|
||||
return tkinter.filedialog.askopenfilename(title=title, filetypes=((t[0], ' '.join(t[1])) for t in filetypes),
|
||||
initialfile=suggest or None)
|
||||
try:
|
||||
return tkinter.filedialog.askopenfilename(
|
||||
title=title,
|
||||
filetypes=((t[0], ' '.join(t[1])) for t in filetypes),
|
||||
initialfile=suggest or None,
|
||||
)
|
||||
finally:
|
||||
root.destroy()
|
||||
|
||||
|
||||
def save_filename(title: str, filetypes: typing.Iterable[typing.Tuple[str, typing.Iterable[str]]], suggest: str = "") \
|
||||
-> typing.Optional[str]:
|
||||
logging.info(f"Opening file save dialog for {title}.")
|
||||
|
||||
def run(*args: str):
|
||||
return subprocess.run(args, capture_output=True, text=True).stdout.split("\n", 1)[0] or None
|
||||
|
||||
if is_linux:
|
||||
# prefer native dialog
|
||||
from shutil import which
|
||||
kdialog = which("kdialog")
|
||||
if kdialog:
|
||||
k_filters = '|'.join((f'{text} (*{" *".join(ext)})' for (text, ext) in filetypes))
|
||||
return run(kdialog, f"--title={title}", "--getsavefilename", suggest or ".", k_filters)
|
||||
return _run_for_stdout(kdialog, f"--title={title}", "--getsavefilename", suggest or ".", k_filters)
|
||||
zenity = which("zenity")
|
||||
if zenity:
|
||||
z_filters = (f'--file-filter={text} ({", ".join(ext)}) | *{" *".join(ext)}' for (text, ext) in filetypes)
|
||||
selection = (f"--filename={suggest}",) if suggest else ()
|
||||
return run(zenity, f"--title={title}", "--file-selection", "--save", *z_filters, *selection)
|
||||
return _run_for_stdout(zenity, f"--title={title}", "--file-selection", "--save", *z_filters, *selection)
|
||||
|
||||
# fall back to tk
|
||||
try:
|
||||
@@ -847,8 +859,14 @@ def save_filename(title: str, filetypes: typing.Iterable[typing.Tuple[str, typin
|
||||
except tkinter.TclError:
|
||||
return None # GUI not available. None is the same as a user clicking "cancel"
|
||||
root.withdraw()
|
||||
return tkinter.filedialog.asksaveasfilename(title=title, filetypes=((t[0], ' '.join(t[1])) for t in filetypes),
|
||||
initialfile=suggest or None)
|
||||
try:
|
||||
return tkinter.filedialog.asksaveasfilename(
|
||||
title=title,
|
||||
filetypes=((t[0], ' '.join(t[1])) for t in filetypes),
|
||||
initialfile=suggest or None,
|
||||
)
|
||||
finally:
|
||||
root.destroy()
|
||||
|
||||
|
||||
def _mp_open_directory(res: "multiprocessing.Queue[typing.Optional[str]]", *args: Any) -> None:
|
||||
@@ -896,6 +914,13 @@ def open_directory(title: str, suggest: str = "") -> typing.Optional[str]:
|
||||
|
||||
|
||||
def messagebox(title: str, text: str, error: bool = False) -> None:
|
||||
if not gui_enabled:
|
||||
if error:
|
||||
logging.error(f"{title}: {text}")
|
||||
else:
|
||||
logging.info(f"{title}: {text}")
|
||||
return
|
||||
|
||||
if is_kivy_running():
|
||||
from kvui import MessageBox
|
||||
MessageBox(title, text, error).open()
|
||||
@@ -931,6 +956,9 @@ def messagebox(title: str, text: str, error: bool = False) -> None:
|
||||
root.update()
|
||||
|
||||
|
||||
gui_enabled = not sys.stdout or "--nogui" not in sys.argv
|
||||
"""Checks if the user wanted no GUI mode and has a terminal to use it with."""
|
||||
|
||||
def title_sorted(data: typing.Iterable, key=None, ignore: typing.AbstractSet[str] = frozenset(("a", "the"))):
|
||||
"""Sorts a sequence of text ignoring typical articles like "a" or "the" in the beginning."""
|
||||
def sorter(element: Union[str, Dict[str, Any]]) -> str:
|
||||
@@ -1050,9 +1078,18 @@ def freeze_support() -> None:
|
||||
_extend_freeze_support()
|
||||
|
||||
|
||||
def visualize_regions(root_region: Region, file_name: str, *,
|
||||
show_entrance_names: bool = False, show_locations: bool = True, show_other_regions: bool = True,
|
||||
linetype_ortho: bool = True, regions_to_highlight: set[Region] | None = None) -> None:
|
||||
def visualize_regions(
|
||||
root_region: Region,
|
||||
file_name: str,
|
||||
*,
|
||||
show_entrance_names: bool = False,
|
||||
show_locations: bool = True,
|
||||
show_other_regions: bool = True,
|
||||
linetype_ortho: bool = True,
|
||||
regions_to_highlight: set[Region] | None = None,
|
||||
entrance_highlighting: dict[int, int] | None = None,
|
||||
detail_other_regions: bool = False,
|
||||
auto_assign_colors: bool = False) -> None:
|
||||
"""Visualize the layout of a world as a PlantUML diagram.
|
||||
|
||||
:param root_region: The region from which to start the diagram from. (Usually the "Menu" region of your world.)
|
||||
@@ -1069,6 +1106,13 @@ def visualize_regions(root_region: Region, file_name: str, *,
|
||||
:param show_other_regions: (default True) If enabled, regions that can't be reached by traversing exits are shown.
|
||||
:param linetype_ortho: (default True) If enabled, orthogonal straight line parts will be used; otherwise polylines.
|
||||
:param regions_to_highlight: Regions that will be highlighted in green if they are reachable.
|
||||
:param entrance_highlighting: a mapping from your world's entrance randomization groups to RGB values, used to color
|
||||
your entrances
|
||||
:param detail_other_regions: (default False) If enabled, will fully visualize regions that aren't reachable
|
||||
from root_region.
|
||||
:param auto_assign_colors: (default False) If enabled, will automatically assign random colors to entrances of the
|
||||
same randomization group. Uses entrance_highlighting first, and only picks random colors for entrance groups
|
||||
not found in the passed-in map
|
||||
|
||||
Example usage in World code:
|
||||
from Utils import visualize_regions
|
||||
@@ -1094,6 +1138,34 @@ def visualize_regions(root_region: Region, file_name: str, *,
|
||||
regions: typing.Deque[Region] = deque((root_region,))
|
||||
multiworld: MultiWorld = root_region.multiworld
|
||||
|
||||
colors_used: set[int] = set()
|
||||
if entrance_highlighting:
|
||||
for color in entrance_highlighting.values():
|
||||
# filter the colors to their most-significant bits to avoid too similar colors
|
||||
colors_used.add(color & 0xF0F0F0)
|
||||
else:
|
||||
# assign an empty dict to not crash later
|
||||
# the parameter is optional for ease of use when you don't care about colors
|
||||
entrance_highlighting = {}
|
||||
|
||||
def select_color(group: int) -> int:
|
||||
# specifically spacing color indexes by three different prime numbers (3, 5, 7) for the RGB components to avoid
|
||||
# obvious cyclical color patterns
|
||||
COLOR_INDEX_SPACING: int = 0x357
|
||||
new_color_index: int = (group * COLOR_INDEX_SPACING) % 0x1000
|
||||
new_color = ((new_color_index & 0xF00) << 12) + \
|
||||
((new_color_index & 0xF0) << 8) + \
|
||||
((new_color_index & 0xF) << 4)
|
||||
while new_color in colors_used:
|
||||
# while this is technically unbounded, expected collisions are low. There are 4095 possible colors
|
||||
# and worlds are unlikely to get to anywhere close to that many entrance groups
|
||||
# intentionally not using multiworld.random to not affect output when debugging with this tool
|
||||
new_color_index += COLOR_INDEX_SPACING
|
||||
new_color = ((new_color_index & 0xF00) << 12) + \
|
||||
((new_color_index & 0xF0) << 8) + \
|
||||
((new_color_index & 0xF) << 4)
|
||||
return new_color
|
||||
|
||||
def fmt(obj: Union[Entrance, Item, Location, Region]) -> str:
|
||||
name = obj.name
|
||||
if isinstance(obj, Item):
|
||||
@@ -1113,18 +1185,28 @@ def visualize_regions(root_region: Region, file_name: str, *,
|
||||
|
||||
def visualize_exits(region: Region) -> None:
|
||||
for exit_ in region.exits:
|
||||
color_code: str = ""
|
||||
if exit_.randomization_group in entrance_highlighting:
|
||||
color_code = f" #{entrance_highlighting[exit_.randomization_group]:0>6X}"
|
||||
if exit_.connected_region:
|
||||
if show_entrance_names:
|
||||
uml.append(f"\"{fmt(region)}\" --> \"{fmt(exit_.connected_region)}\" : \"{fmt(exit_)}\"")
|
||||
uml.append(f"\"{fmt(region)}\" --> \"{fmt(exit_.connected_region)}\" : \"{fmt(exit_)}\"{color_code}")
|
||||
else:
|
||||
try:
|
||||
uml.remove(f"\"{fmt(exit_.connected_region)}\" --> \"{fmt(region)}\"")
|
||||
uml.append(f"\"{fmt(exit_.connected_region)}\" <--> \"{fmt(region)}\"")
|
||||
uml.remove(f"\"{fmt(exit_.connected_region)}\" --> \"{fmt(region)}\"{color_code}")
|
||||
uml.append(f"\"{fmt(exit_.connected_region)}\" <--> \"{fmt(region)}\"{color_code}")
|
||||
except ValueError:
|
||||
uml.append(f"\"{fmt(region)}\" --> \"{fmt(exit_.connected_region)}\"")
|
||||
uml.append(f"\"{fmt(region)}\" --> \"{fmt(exit_.connected_region)}\"{color_code}")
|
||||
else:
|
||||
uml.append(f"circle \"unconnected exit:\\n{fmt(exit_)}\"")
|
||||
uml.append(f"\"{fmt(region)}\" --> \"unconnected exit:\\n{fmt(exit_)}\"")
|
||||
uml.append(f"circle \"unconnected exit:\\n{fmt(exit_)}\" {color_code}")
|
||||
uml.append(f"\"{fmt(region)}\" --> \"unconnected exit:\\n{fmt(exit_)}\"{color_code}")
|
||||
for entrance in region.entrances:
|
||||
color_code: str = ""
|
||||
if entrance.randomization_group in entrance_highlighting:
|
||||
color_code = f" #{entrance_highlighting[entrance.randomization_group]:0>6X}"
|
||||
if not entrance.parent_region:
|
||||
uml.append(f"circle \"unconnected entrance:\\n{fmt(entrance)}\"{color_code}")
|
||||
uml.append(f"\"unconnected entrance:\\n{fmt(entrance)}\" --> \"{fmt(region)}\"{color_code}")
|
||||
|
||||
def visualize_locations(region: Region) -> None:
|
||||
any_lock = any(location.locked for location in region.locations)
|
||||
@@ -1145,9 +1227,27 @@ def visualize_regions(root_region: Region, file_name: str, *,
|
||||
if other_regions := [region for region in multiworld.get_regions(root_region.player) if region not in seen]:
|
||||
uml.append("package \"other regions\" <<Cloud>> {")
|
||||
for region in other_regions:
|
||||
uml.append(f"class \"{fmt(region)}\"")
|
||||
if detail_other_regions:
|
||||
visualize_region(region)
|
||||
else:
|
||||
uml.append(f"class \"{fmt(region)}\"")
|
||||
uml.append("}")
|
||||
|
||||
if auto_assign_colors:
|
||||
all_entrances: list[Entrance] = []
|
||||
for region in multiworld.get_regions(root_region.player):
|
||||
all_entrances.extend(region.entrances)
|
||||
all_entrances.extend(region.exits)
|
||||
all_groups: list[int] = sorted(set([entrance.randomization_group for entrance in all_entrances]))
|
||||
for group in all_groups:
|
||||
if group not in entrance_highlighting:
|
||||
if len(colors_used) >= 0x1000:
|
||||
# on the off chance someone makes 4096 different entrance groups, don't cycle forever
|
||||
break
|
||||
new_color: int = select_color(group)
|
||||
entrance_highlighting[group] = new_color
|
||||
colors_used.add(new_color)
|
||||
|
||||
uml.append("@startuml")
|
||||
uml.append("hide circle")
|
||||
uml.append("hide empty members")
|
||||
@@ -1158,7 +1258,7 @@ def visualize_regions(root_region: Region, file_name: str, *,
|
||||
seen.add(current_region)
|
||||
visualize_region(current_region)
|
||||
regions.extend(exit_.connected_region for exit_ in current_region.exits if exit_.connected_region)
|
||||
if show_other_regions:
|
||||
if show_other_regions or detail_other_regions:
|
||||
visualize_other_regions()
|
||||
uml.append("@enduml")
|
||||
|
||||
@@ -1222,3 +1322,35 @@ class DaemonThreadPoolExecutor(concurrent.futures.ThreadPoolExecutor):
|
||||
t.start()
|
||||
self._threads.add(t)
|
||||
# NOTE: don't add to _threads_queues so we don't block on shutdown
|
||||
|
||||
|
||||
def get_full_typename(t: type) -> str:
|
||||
"""Returns the full qualified name of a type, including its module (if not builtins)."""
|
||||
module = t.__module__
|
||||
if module and module != "builtins":
|
||||
return f"{module}.{t.__qualname__}"
|
||||
return t.__qualname__
|
||||
|
||||
|
||||
def get_all_causes(ex: Exception) -> str:
|
||||
"""Return a string describing the recursive causes of this exception.
|
||||
|
||||
:param ex: The exception to be described.
|
||||
:return A multiline string starting with the initial exception on the first line and each resulting exception
|
||||
on subsequent lines with progressive indentation.
|
||||
|
||||
For example:
|
||||
|
||||
```
|
||||
Exception: Invalid value 'bad'.
|
||||
Which caused: Options.OptionError: Error generating option
|
||||
Which caused: ValueError: File bad.yaml is invalid.
|
||||
```
|
||||
"""
|
||||
cause = ex
|
||||
causes = [f"{get_full_typename(type(ex))}: {ex}"]
|
||||
while cause := cause.__cause__:
|
||||
causes.append(f"{get_full_typename(type(cause))}: {cause}")
|
||||
top = causes[-1]
|
||||
others = "".join(f"\n{' ' * (i + 1)}Which caused: {c}" for i, c in enumerate(reversed(causes[:-1])))
|
||||
return f"{top}{others}"
|
||||
|
||||
@@ -20,7 +20,8 @@ if typing.TYPE_CHECKING:
|
||||
Utils.local_path.cached_path = os.path.dirname(__file__)
|
||||
settings.no_gui = True
|
||||
configpath = os.path.abspath("config.yaml")
|
||||
if not os.path.exists(configpath): # fall back to config.yaml in home
|
||||
if not os.path.exists(configpath):
|
||||
# fall back to config.yaml in user_path if config does not exist in cwd to match settings.py
|
||||
configpath = os.path.abspath(Utils.user_path('config.yaml'))
|
||||
|
||||
|
||||
|
||||
@@ -1,46 +1,20 @@
|
||||
# WebHost
|
||||
|
||||
## Asset License
|
||||
|
||||
The image files used in the page design were specifically designed for archipelago.gg and are **not** covered by the top
|
||||
level LICENSE.
|
||||
See individual LICENSE files in `./static/static/**`.
|
||||
|
||||
You are only allowed to use them for personal use, testing and development.
|
||||
If the site is reachable over the internet, have a robots.txt in place (see `ASSET_RIGHTS` in `config.yaml`)
|
||||
and do not promote it publicly. Alternatively replace or remove the assets.
|
||||
|
||||
## Contribution Guidelines
|
||||
**Thank you for your interest in contributing to the Archipelago website!**
|
||||
Much of the content on the website is generated automatically, but there are some things
|
||||
that need a personal touch. For those things, we rely on contributions from both the core
|
||||
team and the community. The current primary maintainer of the website is Farrak Kilhn.
|
||||
He may be found on Discord as `Farrak Kilhn#0418`, or on GitHub as `LegendaryLinux`.
|
||||
|
||||
### Small Changes
|
||||
Little changes like adding a button or a couple new select elements are perfectly fine.
|
||||
Tweaks to style specific to a PR's content are also probably not a problem. For example, if
|
||||
you build a new page which needs two side by side tables, and you need to write a CSS file
|
||||
specific to your page, that is perfectly reasonable.
|
||||
Pages should preferably be rendered on the server side with Jinja. Features should work with noscript if feasible.
|
||||
Design changes have to fit the overall design.
|
||||
|
||||
### Content Additions
|
||||
Once you develop a new feature or add new content the website, make a pull request. It will
|
||||
be reviewed by the community and there will probably be some discussion around it. Depending
|
||||
on the size of the feature, and if new styles are required, there may be an additional step
|
||||
before the PR is accepted wherein Farrak works with the designer to implement styles.
|
||||
Introduction of JS dependencies should first be discussed on Discord or in a draft PR.
|
||||
|
||||
### Restrictions on Style Changes
|
||||
A professional designer is paid to develop the styles and assets for the Archipelago website.
|
||||
In an effort to maintain a consistent look and feel, pull requests which *exclusively*
|
||||
change site styles are rejected. Please note this applies to code which changes the overall
|
||||
look and feel of the site, not to small tweaks to CSS for your custom page. The intention
|
||||
behind these restrictions is to maintain a curated feel for the design of the site. If
|
||||
any PR affects the overall feel of the site but includes additive changes, there will
|
||||
likely be a conversation about how to implement those changes without compromising the
|
||||
curated site style. It is therefore worth noting there are a couple files which, if
|
||||
changed in your pull request, will cause it to draw additional scrutiny.
|
||||
|
||||
These closely guarded files are:
|
||||
- `globalStyles.css`
|
||||
- `islandFooter.css`
|
||||
- `landing.css`
|
||||
- `markdown.css`
|
||||
- `tooltip.css`
|
||||
|
||||
### Site Themes
|
||||
There are several themes available for game pages. It is possible to request a new theme in
|
||||
the `#art-and-design` channel on Discord. Because themes are created by the designer, they
|
||||
are not free, and take some time to create. Farrak works closely with the designer to implement
|
||||
these themes, and pays for the assets out of pocket. Therefore, only a couple themes per year
|
||||
are added. If a proposed theme seems like a cool idea and the community likes it, there is a
|
||||
good chance it will become a reality.
|
||||
See also [docs/style.md](/docs/style.md) for the style guide.
|
||||
|
||||
@@ -23,6 +23,17 @@ app.jinja_env.filters['any'] = any
|
||||
app.jinja_env.filters['all'] = all
|
||||
app.jinja_env.filters['get_file_safe_name'] = get_file_safe_name
|
||||
|
||||
# overwrites of flask default config
|
||||
app.config["DEBUG"] = False
|
||||
app.config["PORT"] = 80
|
||||
app.config["UPLOAD_FOLDER"] = UPLOAD_FOLDER
|
||||
app.config["MAX_CONTENT_LENGTH"] = 64 * 1024 * 1024 # 64 megabyte limit
|
||||
# if you want to deploy, make sure you have a non-guessable secret key
|
||||
app.config["SECRET_KEY"] = bytes(socket.gethostname(), encoding="utf-8")
|
||||
app.config["SESSION_PERMANENT"] = True
|
||||
app.config["MAX_FORM_MEMORY_SIZE"] = 2 * 1024 * 1024 # 2 MB, needed for large option pages such as SC2
|
||||
|
||||
# custom config
|
||||
app.config["SELFHOST"] = True # application process is in charge of running the websites
|
||||
app.config["GENERATORS"] = 8 # maximum concurrent world gens
|
||||
app.config["HOSTERS"] = 8 # maximum concurrent room hosters
|
||||
@@ -30,19 +41,12 @@ app.config["SELFLAUNCH"] = True # application process is in charge of launching
|
||||
app.config["SELFLAUNCHCERT"] = None # can point to a SSL Certificate to encrypt Room websocket connections
|
||||
app.config["SELFLAUNCHKEY"] = None # can point to a SSL Certificate Key to encrypt Room websocket connections
|
||||
app.config["SELFGEN"] = True # application process is in charge of scheduling Generations.
|
||||
app.config["DEBUG"] = False
|
||||
app.config["PORT"] = 80
|
||||
app.config['UPLOAD_FOLDER'] = UPLOAD_FOLDER
|
||||
app.config['MAX_CONTENT_LENGTH'] = 64 * 1024 * 1024 # 64 megabyte limit
|
||||
# if you want to deploy, make sure you have a non-guessable secret key
|
||||
app.config["SECRET_KEY"] = bytes(socket.gethostname(), encoding="utf-8")
|
||||
# at what amount of worlds should scheduling be used, instead of rolling in the web-thread
|
||||
app.config["JOB_THRESHOLD"] = 1
|
||||
# after what time in seconds should generation be aborted, freeing the queue slot. Can be set to None to disable.
|
||||
app.config["JOB_TIME"] = 600
|
||||
# memory limit for generator processes in bytes
|
||||
app.config["GENERATOR_MEMORY_LIMIT"] = 4294967296
|
||||
app.config['SESSION_PERMANENT'] = True
|
||||
|
||||
# waitress uses one thread for I/O, these are for processing of views that then get sent
|
||||
# archipelago.gg uses gunicorn + nginx; ignoring this option
|
||||
|
||||
@@ -89,19 +89,24 @@ class WebHostContext(Context):
|
||||
setattr(self, key, value)
|
||||
self.non_hintable_names = collections.defaultdict(frozenset, self.non_hintable_names)
|
||||
|
||||
def listen_to_db_commands(self):
|
||||
async def listen_to_db_commands(self):
|
||||
cmdprocessor = DBCommandProcessor(self)
|
||||
|
||||
while not self.exit_event.is_set():
|
||||
with db_session:
|
||||
commands = select(command for command in Command if command.room.id == self.room_id)
|
||||
if commands:
|
||||
for command in commands:
|
||||
self.main_loop.call_soon_threadsafe(cmdprocessor, command.commandtext)
|
||||
command.delete()
|
||||
commit()
|
||||
del commands
|
||||
time.sleep(5)
|
||||
await self.main_loop.run_in_executor(None, self._process_db_commands, cmdprocessor)
|
||||
try:
|
||||
await asyncio.wait_for(self.exit_event.wait(), 5)
|
||||
except asyncio.TimeoutError:
|
||||
pass
|
||||
|
||||
def _process_db_commands(self, cmdprocessor):
|
||||
with db_session:
|
||||
commands = select(command for command in Command if command.room.id == self.room_id)
|
||||
if commands:
|
||||
for command in commands:
|
||||
self.main_loop.call_soon_threadsafe(cmdprocessor, command.commandtext)
|
||||
command.delete()
|
||||
commit()
|
||||
|
||||
@db_session
|
||||
def load(self, room_id: int):
|
||||
@@ -156,9 +161,9 @@ class WebHostContext(Context):
|
||||
with db_session:
|
||||
savegame_data = Room.get(id=self.room_id).multisave
|
||||
if savegame_data:
|
||||
self.set_save(restricted_loads(Room.get(id=self.room_id).multisave))
|
||||
self.set_save(restricted_loads(savegame_data))
|
||||
self._start_async_saving(atexit_save=False)
|
||||
threading.Thread(target=self.listen_to_db_commands, daemon=True).start()
|
||||
asyncio.create_task(self.listen_to_db_commands())
|
||||
|
||||
@db_session
|
||||
def _save(self, exit_save: bool = False) -> bool:
|
||||
@@ -229,6 +234,17 @@ def set_up_logging(room_id) -> logging.Logger:
|
||||
return logger
|
||||
|
||||
|
||||
def tear_down_logging(room_id):
|
||||
"""Close logging handling for a room."""
|
||||
logger_name = f"RoomLogger {room_id}"
|
||||
if logger_name in logging.Logger.manager.loggerDict:
|
||||
logger = logging.getLogger(logger_name)
|
||||
for handler in logger.handlers[:]:
|
||||
logger.removeHandler(handler)
|
||||
handler.close()
|
||||
del logging.Logger.manager.loggerDict[logger_name]
|
||||
|
||||
|
||||
def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
|
||||
cert_file: typing.Optional[str], cert_key_file: typing.Optional[str],
|
||||
host: str, rooms_to_run: multiprocessing.Queue, rooms_shutting_down: multiprocessing.Queue):
|
||||
@@ -325,7 +341,7 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
|
||||
|
||||
except (KeyboardInterrupt, SystemExit):
|
||||
if ctx.saving:
|
||||
ctx._save()
|
||||
ctx._save(True)
|
||||
setattr(asyncio.current_task(), "save", None)
|
||||
except Exception as e:
|
||||
with db_session:
|
||||
@@ -336,19 +352,25 @@ def run_server_process(name: str, ponyconfig: dict, static_server_data: dict,
|
||||
raise
|
||||
else:
|
||||
if ctx.saving:
|
||||
ctx._save()
|
||||
ctx._save(True)
|
||||
setattr(asyncio.current_task(), "save", None)
|
||||
finally:
|
||||
try:
|
||||
ctx.save_dirty = False # make sure the saving thread does not write to DB after final wakeup
|
||||
ctx.exit_event.set() # make sure the saving thread stops at some point
|
||||
# NOTE: async saving should probably be an async task and could be merged with shutdown_task
|
||||
|
||||
if ctx.server and hasattr(ctx.server, "ws_server"):
|
||||
ctx.server.ws_server.close()
|
||||
await ctx.server.ws_server.wait_closed()
|
||||
|
||||
with db_session:
|
||||
# ensure the Room does not spin up again on its own, minute of safety buffer
|
||||
room = Room.get(id=room_id)
|
||||
room.last_activity = datetime.datetime.utcnow() - \
|
||||
datetime.timedelta(minutes=1, seconds=room.timeout)
|
||||
del room
|
||||
tear_down_logging(room_id)
|
||||
logging.info(f"Shutting down room {room_id} on {name}.")
|
||||
finally:
|
||||
await asyncio.sleep(5)
|
||||
|
||||
@@ -128,8 +128,13 @@ def tutorial_landing():
|
||||
"authors": tutorial.authors,
|
||||
"language": tutorial.language
|
||||
}
|
||||
tutorials = {world_name: tutorials for world_name, tutorials in title_sorted(
|
||||
tutorials.items(), key=lambda element: "\x00" if element[0] == "Archipelago" else worlds[element[0]].game)}
|
||||
|
||||
worlds = dict(
|
||||
title_sorted(
|
||||
worlds.items(), key=lambda element: "\x00" if element[0] == "Archipelago" else worlds[element[0]].game
|
||||
)
|
||||
)
|
||||
|
||||
return render_template("tutorialLanding.html", worlds=worlds, tutorials=tutorials)
|
||||
|
||||
|
||||
|
||||
@@ -55,6 +55,9 @@
|
||||
{{ OptionTitle(option_name, option) }}
|
||||
<div class="named-range-container">
|
||||
<select id="{{ option_name }}-select" name="{{ option_name }}" data-option-name="{{ option_name }}" {{ "disabled" if option.default == "random" }}>
|
||||
{% if option.default not in option.special_range_names.values() %}
|
||||
<option value="{{ option.default }}" selected>Default ({{ option.default }})</option>
|
||||
{% endif %}
|
||||
{% for key, val in option.special_range_names.items() %}
|
||||
{% if option.default == val %}
|
||||
<option value="{{ val }}" selected>{{ key|replace("_", " ")|title }} ({{ val }})</option>
|
||||
@@ -94,6 +97,9 @@
|
||||
<div class="text-choice-container">
|
||||
<div class="text-choice-wrapper">
|
||||
<select id="{{ option_name }}" name="{{ option_name }}" {{ "disabled" if option.default == "random" }}>
|
||||
{% if option.default not in option.options.values() %}
|
||||
<option value="{{ option.default }}" selected>Default ({{ option.default }})</option>
|
||||
{% endif %}
|
||||
{% for id, name in option.name_lookup.items()|sort %}
|
||||
{% if name != "random" %}
|
||||
{% if option.default == id %}
|
||||
|
||||
13
data/GLOBAL.apignore
Normal file
13
data/GLOBAL.apignore
Normal file
@@ -0,0 +1,13 @@
|
||||
# This file specifies patterns that are ignored by default for any world built with the "Build APWorlds" component.
|
||||
# These patterns can be overriden by a world-specific .apignore using !-prefixed patterns for negation.
|
||||
|
||||
# Auto-created folders
|
||||
__MACOSX
|
||||
.DS_Store
|
||||
__pycache__
|
||||
|
||||
# Unneeded files
|
||||
/archipelago.json
|
||||
/.apignore
|
||||
/.git
|
||||
/.gitignore
|
||||
@@ -28,7 +28,7 @@
|
||||
name: Player{number}
|
||||
|
||||
# Used to describe your yaml. Useful if you have multiple files.
|
||||
description: {{ yaml_dump("Default %s Template" % game) }}
|
||||
description: {{ yaml_dump("%s Preset for %s" % (preset_name, game)) if preset_name else yaml_dump("Default %s Template" % game) }}
|
||||
|
||||
game: {{ yaml_dump(game) }}
|
||||
requires:
|
||||
@@ -38,11 +38,11 @@ requires:
|
||||
{{ yaml_dump(game) }}: {{ world_version }} # Version of the world required for this yaml to work as expected.
|
||||
{%- endif %}
|
||||
|
||||
{%- macro range_option(option) %}
|
||||
{%- macro range_option(option, option_val) %}
|
||||
# You can define additional values between the minimum and maximum values.
|
||||
# Minimum value is {{ option.range_start }}
|
||||
# Maximum value is {{ option.range_end }}
|
||||
{%- set data, notes = dictify_range(option) %}
|
||||
{%- set data, notes = dictify_range(option, option_val) %}
|
||||
{%- for entry, default in data.items() %}
|
||||
{{ entry }}: {{ default }}{% if notes[entry] %} # {{ notes[entry] }}{% endif %}
|
||||
{%- endfor -%}
|
||||
@@ -56,6 +56,10 @@ requires:
|
||||
|
||||
{%- for option_key, option in group_options.items() %}
|
||||
{{ option_key }}:
|
||||
{%- set option_val = option.default %}
|
||||
{%- if option_key in preset %}
|
||||
{%- set option_val = preset[option_key] %}
|
||||
{%- endif -%}
|
||||
{%- if option.__doc__ %}
|
||||
# {{ cleandoc(option.__doc__)
|
||||
| trim
|
||||
@@ -69,25 +73,25 @@ requires:
|
||||
{%- endif -%}
|
||||
|
||||
{%- if option.range_start is defined and option.range_start is number %}
|
||||
{{- range_option(option) -}}
|
||||
{{- range_option(option, option_val) -}}
|
||||
|
||||
{%- elif option.options -%}
|
||||
{%- for suboption_option_id, sub_option_name in option.name_lookup.items() %}
|
||||
{{ yaml_dump(sub_option_name) }}: {% if suboption_option_id == option.default %}50{% else %}0{% endif %}
|
||||
{{ yaml_dump(sub_option_name) }}: {% if suboption_option_id == option_val or sub_option_name == option_val %}50{% else %}0{% endif %}
|
||||
{%- endfor -%}
|
||||
|
||||
{%- if option.name_lookup[option.default] not in option.options %}
|
||||
{{ yaml_dump(option.default) }}: 50
|
||||
|
||||
{%- if option.name_lookup[option_val] not in option.options and option_val not in option.options %}
|
||||
{{ yaml_dump(option_val) }}: 50
|
||||
{%- endif -%}
|
||||
|
||||
{%- elif option.default is string %}
|
||||
{{ yaml_dump(option.default) }}: 50
|
||||
{%- elif option_val is string %}
|
||||
{{ yaml_dump(option_val) }}: 50
|
||||
|
||||
{%- elif option.default is iterable and option.default is not mapping %}
|
||||
{{ option.default | list }}
|
||||
{%- elif option_val is iterable and option_val is not mapping %}
|
||||
{{ option_val | list }}
|
||||
|
||||
{%- else %}
|
||||
{{ yaml_dump(option.default) | indent(4, first=false) }}
|
||||
{{ yaml_dump(option_val) | indent(4, first=false) }}
|
||||
{%- endif -%}
|
||||
{{ "\n" }}
|
||||
{%- endfor %}
|
||||
|
||||
@@ -70,6 +70,9 @@
|
||||
# DOOM II
|
||||
/worlds/doom_ii/ @Daivuk @KScl
|
||||
|
||||
# EarthBound
|
||||
/worlds/earthbound/ @PinkSwitch
|
||||
|
||||
# Factorio
|
||||
/worlds/factorio/ @Berserker66
|
||||
|
||||
@@ -176,8 +179,12 @@
|
||||
# Sonic Adventure 2 Battle
|
||||
/worlds/sa2b/ @PoryGone @RaspberrySpace
|
||||
|
||||
# Satisfactory
|
||||
/worlds/satisfactory/ @Jarno458 @budak7273
|
||||
|
||||
# Starcraft 2
|
||||
/worlds/sc2/ @Ziktofel
|
||||
# Note: @Ziktofel acts as a mentor
|
||||
/worlds/sc2/ @MatthewMarinets @Snarkie @SirChuckOfTheChuckles
|
||||
|
||||
# Super Metroid
|
||||
/worlds/sm/ @lordlou
|
||||
|
||||
@@ -17,7 +17,8 @@ it will not be detailed here.
|
||||
The client is an intermediary program between the game and the Archipelago server. This can either be a direct
|
||||
modification to the game, an external program, or both. This can be implemented in nearly any modern language, but it
|
||||
must fulfill a few requirements in order to function as expected. Libraries for most modern languages and the spec for
|
||||
various packets can be found in the [network protocol](/docs/network%20protocol.md) API reference document.
|
||||
various packets can be found in the [network protocol](/docs/network%20protocol.md) API reference document. Additional help with specific game
|
||||
engines and rom formats can be found in the #ap-modding-help channel in the [Discord](https://archipelago.gg/discord).
|
||||
|
||||
### Hard Requirements
|
||||
|
||||
@@ -139,8 +140,8 @@ if possible.
|
||||
|
||||
* An implementation of
|
||||
[get_filler_item_name](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L473)
|
||||
* By default, this function chooses any item name from `item_name_to_id`, so you want to limit it to only the true
|
||||
filler items.
|
||||
* By default, this function chooses any item name from `item_name_to_id`, which may include items you consider
|
||||
"non-repeatable".
|
||||
* An `options_dataclass` defining the options players have available to them
|
||||
* This should be accompanied by a type hint for `options` with the same class name
|
||||
* A [bug report page](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L220)
|
||||
|
||||
@@ -41,7 +41,7 @@ There are also the following optional fields:
|
||||
If the APWorld is packaged as an `.apworld` zip file, it also needs to have `version` and `compatible_version`,
|
||||
which refer to the version of the APContainer packaging scheme defined in [Files.py](../worlds/Files.py).
|
||||
These get automatically added to the `archipelago.json` of an .apworld if it is packaged using the
|
||||
["Build apworlds" launcher component](#build-apworlds-launcher-component),
|
||||
["Build APWorlds" launcher component](#build-apworlds-launcher-component),
|
||||
which is the correct way to package your `.apworld` as a world developer. Do not write these fields yourself.
|
||||
|
||||
### "Build APWorlds" Launcher Component
|
||||
@@ -50,7 +50,9 @@ In the Archipelago Launcher, there is a "Build APWorlds" component that will pac
|
||||
and add `archipelago.json` manifest files to them.
|
||||
These .apworld files will be output to `build/apworlds` (relative to the Archipelago root directory).
|
||||
The `archipelago.json` file in each .apworld will automatically include the appropriate
|
||||
`version` and `compatible_version`.
|
||||
`version` and `compatible_version`.
|
||||
The component can also be called from the command line to allow for specifying a certain list of worlds to build.
|
||||
For example, running `Launcher.py "Build APWorlds" -- "Game Name"` will build only the game called `Game Name`.
|
||||
|
||||
If a world folder has an `archipelago.json` in its root, any fields it contains will be carried over.
|
||||
So, a world folder with an `archipelago.json` that looks like this:
|
||||
@@ -79,10 +81,26 @@ will be packaged into an `.apworld` with a manifest file inside of it that looks
|
||||
|
||||
This is the recommended workflow for packaging your world to an `.apworld`.
|
||||
|
||||
## Extra Data
|
||||
### .apignore Exclusions
|
||||
|
||||
The zip can contain arbitrary files in addition what was specified above.
|
||||
By default, any additional files inside of the world folder will be packaged into the resulting `.apworld` archive and
|
||||
can then be read by the world. However, if there are any other files that aren't needed in the resulting `.apworld`, you
|
||||
can automatically prevent the build component from including them by specifying them in a file called `.apignore` inside
|
||||
the root of the world folder.
|
||||
|
||||
The `.apignore` file selects files in the same way as the `.gitignore` format with patterns separated by line describing
|
||||
which files to ignore. For example, an `.apignore` like this:
|
||||
|
||||
```gitignore
|
||||
*.iso
|
||||
scripts/
|
||||
!scripts/needed.py
|
||||
```
|
||||
|
||||
would ignore any `.iso` files and anything in the scripts folder except for `scripts/needed.py`.
|
||||
|
||||
Some exclusions are made by default for all worlds such as `__pycache__` folders. These are listed in the
|
||||
`GLOBAL.apignore` file inside of the `data` directory.
|
||||
|
||||
## Caveats
|
||||
|
||||
|
||||
@@ -6,6 +6,49 @@ including [Contributing](contributing.md), [Adding Games](<adding games.md>), an
|
||||
|
||||
---
|
||||
|
||||
### I've never added a game to Archipelago before. Should I start with the APWorld or the game client?
|
||||
|
||||
Strictly speaking, this is a false dichotomy: we do *not* recommend doing 100% of client work before the APWorld,
|
||||
or 100% of APWorld work before the client. It's important to iterate on both parts and test them together.
|
||||
However, the early iterations tend to be very similar for most games,
|
||||
so the typical recommendation for first-time AP developers is:
|
||||
|
||||
- Start with a proof-of-concept for [the game client](adding%20games.md#client)
|
||||
- Figure out how to interface with the game. Whether that means "modding" the game, or patching a ROM file,
|
||||
or developing a separate client program that edits the game's memory, or some other technique.
|
||||
- Figure out how to give items and detect locations in the actual game. Not every item and location,
|
||||
just one of each major type (e.g. opening a chest vs completing a sidequest) to prove all the items and locations
|
||||
you want can actually be implemented.
|
||||
- Figure out how to make a websocket connection to an AP server, possibly using a client library (see [Network Protocol](<network%20protocol.md>).
|
||||
To make absolutely sure this part works, you may want to test the connection by generating a multiworld
|
||||
with a different game, then making your client temporarily pretend to be that other game.
|
||||
- Next, make a "trivial" APWorld, i.e. an APWorld that always generates the same items and locations
|
||||
- If you've never done this before, likely the fastest approach is to copy-paste [APQuest](<../worlds/apquest>), and read the many
|
||||
comments in there until you understand how to edit the items and locations.
|
||||
- Then you can do your first "end-to-end test": generate a multiworld using your APWorld, [run a local server](<running%20from%20source.md>)
|
||||
to host it, connect to that local server from your game client, actually check a location in the game,
|
||||
and finally make sure the client successfully sent that location check to the AP server
|
||||
as well as received an item from it.
|
||||
|
||||
That's about where general recommendations end. What you should do next will depend entirely on your game
|
||||
(e.g. implement more items, write down logic rules, add client features, prototype a tracker, etc).
|
||||
If you're not sure, then this would be a good time to re-read [Adding Games](<adding%20games.md>), and [World API](<world%20api.md>).
|
||||
|
||||
There are a few assumptions in this recommendation worth stating explicitly, namely:
|
||||
|
||||
- If something you want to do is infeasible, you want to find out that it's infeasible as soon as possible, before
|
||||
you write a bunch of code assuming it could be done. That's why we recommend starting with the game client.
|
||||
- Getting an APWorld to generate whatever items/locations you want is always feasible, since items/locations are
|
||||
little more than id numbers and name strings during generation.
|
||||
- You generally want to get to an "end-to-end playable" prototype quickly. On top of all the technical challenges these
|
||||
docs describe, it's also important to check that a randomizer is *fun to play*, and figure out what features would be
|
||||
essential for a public release.
|
||||
- A first-time world developer may or may not be deeply familiar with Archipelago, but they're almost certainly familiar
|
||||
with the game they want to randomize. So judging whether your game client is working correctly might be significantly
|
||||
easier than judging if your APWorld is working.
|
||||
|
||||
---
|
||||
|
||||
### My game has a restrictive start that leads to fill errors
|
||||
|
||||
A "restrictive start" here means having a combination of very few sphere 1 locations and potentially requiring more
|
||||
@@ -140,3 +183,58 @@ So when the game itself does not follow this assumption, the options are:
|
||||
- For connections, any logical regions will still need to be reachable through other, *repeatable* connections
|
||||
- For locations, this may require game changes to remove the vanilla item if it affects logic
|
||||
- Decide that resetting the save file is part of the game's logic, and warn players about that
|
||||
|
||||
---
|
||||
|
||||
### What are "local" vs "remote" items, and what are the pros and cons of each?
|
||||
|
||||
First off, these terms can be misleading. Since the whole point of a multi-game multiworld randomizer is that some items
|
||||
are going to be placed in other slots (unless there's only one slot), the choice isn't really "local vs remote";
|
||||
it's "mixed local/remote vs all remote". You have to get "remote items" working to be an AP implementation at all, and
|
||||
it's often simpler to handle every item/location the same way, so you generally shouldn't worry about "local items"
|
||||
until you've finished more critical features.
|
||||
|
||||
Next, "local" and "remote" items confusingly refer to multiple concepts, so it's important to clearly separate them:
|
||||
|
||||
- Whether an item happens to get placed in the same slot it originates from, or a different slot. I'll call these
|
||||
"locally placed" and "remotely placed" items.
|
||||
- Whether an AP client implements location checking for locally placed items by skipping the usual AP server roundtrip
|
||||
(i.e. sending [LocationChecks](<network%20protocol.md#locationchecks>)
|
||||
then receiving [ReceivedItems](<network%20protocol.md#receiveditems>)
|
||||
) and directly giving the item to the player, or by doing the AP server roundtrip regardless. I'll call these
|
||||
"locally implemented" items and "remotely implemented" items.
|
||||
- Locally implementing items requires the AP client to know what the locally placed items were without asking an AP
|
||||
server (or else you'd effectively be doing remote items with extra steps). Typically, it gets that information from
|
||||
a patch file, which is one reason why games that already need a patch file are more likely to choose local items.
|
||||
- If items are remotely implemented, the AP client can use [location scouts](<network%20protocol.md#LocationScouts>)
|
||||
to learn what items are placed on what locations. Features that require this information are sometimes mistakenly
|
||||
assumed to require locally implemented items, but location scouts work just as well as patch file data.
|
||||
- [The `items_handling` bitflags in the Connect packet](<network%20protocol.md#items_handling-flags>).
|
||||
AP clients with remotely implemented items will typically set all three flags, including "from your own world".
|
||||
Clients with locally implemented items might set only the "from other worlds" flag.
|
||||
- Whether a local items client sets the "starting inventory" flag likely depends on other details. For example, if a ROM
|
||||
is being patched, and starting inventory can be added to that patch, then it makes sense to leave the flag unset.
|
||||
|
||||
When people talk about "local vs remote items" as a choice that world devs have to make, they mean deciding whether
|
||||
your client will locally or remotely implement the items which happen to be locally placed (or make both
|
||||
implementations, or let the player choose an implementation).
|
||||
|
||||
Theoretically, the biggest benefit of "local items" is that it allows a solo (single slot) multiworld to be played
|
||||
entirely offline, with no AP server, from start to finish. This is similar to a "standalone"/non-AP randomizer,
|
||||
except that you still get AP's player options, generation, etc. for free.
|
||||
For some games, there are also technical constraints that make certain items easier to implement locally,
|
||||
or less glitchy when implemented locally, as long as you're okay with never allowing these items to be placed remotely
|
||||
(or offering the player even more options).
|
||||
|
||||
The main downside (besides more implementation work) is that "local items" can't support "same slot co-op".
|
||||
That's when two players on two different machines connect to the same slot and play together.
|
||||
This only works if both players receive all the items for that slot, including ones found by the other player,
|
||||
which requires those items to be implemented remotely so the AP server can send them to all of that slot's clients.
|
||||
|
||||
So to recap:
|
||||
|
||||
- (All) remote items is often the simplest choice, since you have to implement remote items anyway.
|
||||
- Remote items enable same slot co-op.
|
||||
- Local items enable solo offline play.
|
||||
- If you want to support both solo offline play and same slot co-op,
|
||||
you might need to expose local vs remote items as an option to the player.
|
||||
|
||||
@@ -20,7 +20,7 @@ game contributions:
|
||||
It is recommended that automated github actions are turned on in your fork to have github run unit tests after
|
||||
pushing.
|
||||
You can turn them on here:
|
||||

|
||||

|
||||
|
||||
* **When reviewing PRs, please leave a message about what was done.**
|
||||
We don't have full test coverage, so manual testing can help.
|
||||
|
||||
@@ -225,7 +225,7 @@ Sent to clients after a client requested this message be sent to them, more info
|
||||
| games | list\[str\] | Optional. Game names this message is targeting |
|
||||
| slots | list\[int\] | Optional. Player slot IDs that this message is targeting |
|
||||
| tags | list\[str\] | Optional. Client [Tags](#Tags) this message is targeting |
|
||||
| data | dict | The data in the [Bounce](#Bounce) package copied |
|
||||
| data | dict | Optional. The data in the [Bounce](#Bounce) package copied |
|
||||
|
||||
### InvalidPacket
|
||||
Sent to clients if the server caught a problem with a packet. This only occurs for errors that are explicitly checked for.
|
||||
@@ -425,7 +425,7 @@ the server will forward the message to all those targets to which any one requir
|
||||
| games | list\[str\] | Optional. Game names that should receive this message |
|
||||
| slots | list\[int\] | Optional. Player IDs that should receive this message |
|
||||
| tags | list\[str\] | Optional. Client tags that should receive this message |
|
||||
| data | dict | Any data you want to send |
|
||||
| data | dict | Optional. Any data you want to send |
|
||||
|
||||
### Get
|
||||
Used to request a single or multiple values from the server's data storage, see the [Set](#Set) package for how to write values to the data storage. A Get package will be answered with a [Retrieved](#Retrieved) package.
|
||||
|
||||
482
docs/rule builder.md
Normal file
482
docs/rule builder.md
Normal file
@@ -0,0 +1,482 @@
|
||||
# Rule Builder
|
||||
|
||||
This document describes the API provided for the rule builder. Using this API provides you with with a simple interface to define rules and the following advantages:
|
||||
|
||||
- Rule classes that avoid all the common pitfalls
|
||||
- Logic optimization
|
||||
- Automatic result caching (opt-in)
|
||||
- Serialization/deserialization
|
||||
- Human-readable logic explanations for players
|
||||
|
||||
## Overview
|
||||
|
||||
The rule builder consists of 3 main parts:
|
||||
|
||||
1. The rules, which are classes that inherit from `rule_builder.rules.Rule`. These are what you write for your logic. They can be combined and take into account your world's options. There are a number of default rules listed below, and you can create as many custom rules for your world as needed. When assigning the rules to a location or entrance they must be resolved.
|
||||
1. Resolved rules, which are classes that inherit from `rule_builder.rules.Rule.Resolved`. These are the optimized rules specific to one player that are set as a location or entrance's access rule. You generally shouldn't be directly creating these but they'll be created when assigning rules to locations or entrances. These are what power the human-readable logic explanations.
|
||||
1. The optional rule builder world subclass `CachedRuleBuilderWorld`, which is a class your world can inherit from instead of `World`. It adds a caching system to the rules that will lazy evaluate and cache the result.
|
||||
|
||||
## Usage
|
||||
|
||||
For the most part the only difference in usage is instead of writing lambdas for your logic, you write static Rule objects. You then must use `world.set_rule` to assign the rule to a location or entrance.
|
||||
|
||||
```python
|
||||
# In your world's create_regions method
|
||||
location = MyWorldLocation(...)
|
||||
self.set_rule(location, Has("A Big Gun"))
|
||||
```
|
||||
|
||||
The rule builder comes with a number of rules by default:
|
||||
|
||||
- `True_`: Always returns true
|
||||
- `False_`: Always returns false
|
||||
- `And`: Checks that all child rules are true (also provided by `&` operator)
|
||||
- `Or`: Checks that at least one child rule is true (also provided by `|` operator)
|
||||
- `Has`: Checks that the player has the given item with the given count (default 1)
|
||||
- `HasAll`: Checks that the player has all given items
|
||||
- `HasAny`: Checks that the player has at least one of the given items
|
||||
- `HasAllCounts`: Checks that the player has all of the counts for the given items
|
||||
- `HasAnyCount`: Checks that the player has any of the counts for the given items
|
||||
- `HasFromList`: Checks that the player has some number of given items
|
||||
- `HasFromListUnique`: Checks that the player has some number of given items, ignoring duplicates of the same item
|
||||
- `HasGroup`: Checks that the player has some number of items from a given item group
|
||||
- `HasGroupUnique`: Checks that the player has some number of items from a given item group, ignoring duplicates of the same item
|
||||
- `CanReachLocation`: Checks that the player can logically reach the given location
|
||||
- `CanReachRegion`: Checks that the player can logically reach the given region
|
||||
- `CanReachEntrance`: Checks that the player can logically reach the given entrance
|
||||
|
||||
You can combine these rules together to describe the logic required for something. For example, to check if a player either has `Movement ability` or they have both `Key 1` and `Key 2`, you can do:
|
||||
|
||||
```python
|
||||
rule = Has("Movement ability") | HasAll("Key 1", "Key 2")
|
||||
```
|
||||
|
||||
> ⚠️ Composing rules with the `and` and `or` keywords will not work. You must use the bitwise `&` and `|` operators. In order to catch mistakes, the rule builder will not let you do boolean operations. As a consequence, in order to check if a rule is defined you must use `if rule is not None`.
|
||||
|
||||
### Assigning rules
|
||||
|
||||
When assigning the rule you must use the `set_rule` helper to correctly resolve and register the rule.
|
||||
|
||||
```python
|
||||
self.set_rule(location_or_entrance, rule)
|
||||
```
|
||||
|
||||
There is also a `create_entrance` helper that will resolve the rule, check if it's `False`, and if not create the entrance and set the rule. This allows you to skip creating entrances that will never be valid. You can also specify `force_creation=True` if you would like to create the entrance even if the rule is `False`.
|
||||
|
||||
```python
|
||||
self.create_entrance(from_region, to_region, rule)
|
||||
```
|
||||
|
||||
> ⚠️ If you use a `CanReachLocation` rule on an entrance, you will either have to create the locations first, or specify the location's parent region name with the `parent_region_name` argument of `CanReachLocation`.
|
||||
|
||||
You can also set a rule for your world's completion condition:
|
||||
|
||||
```python
|
||||
self.set_completion_rule(rule)
|
||||
```
|
||||
|
||||
### Restricting options
|
||||
|
||||
Every rule allows you to specify which options it's applicable for. You can provide the argument `options` which is an iterable of `OptionFilter` instances. Rules that pass the options check will be resolved as normal, and those that fail will be resolved as `False`.
|
||||
|
||||
If you want a comparison that isn't equals, you can specify with the `operator` argument. The following operators are allowed:
|
||||
|
||||
- `eq`: `==`
|
||||
- `ne`: `!=`
|
||||
- `gt`: `>`
|
||||
- `lt`: `<`
|
||||
- `ge`: `>=`
|
||||
- `le`: `<=`
|
||||
- `contains`: `in`
|
||||
|
||||
By default rules that are excluded by their options will default to `False`. If you want to default to `True` instead, you can specify `filtered_resolution=True` on your rule.
|
||||
|
||||
To check if the player can reach a switch, or if they've received the switch item if switches are randomized:
|
||||
|
||||
```python
|
||||
rule = (
|
||||
Has("Red switch", options=[OptionFilter(SwitchRando, 1)])
|
||||
| CanReachLocation("Red switch", options=[OptionFilter(SwitchRando, 0)])
|
||||
)
|
||||
```
|
||||
|
||||
To add an extra logic requirement on the easiest difficulty which is ignored for other difficulties:
|
||||
|
||||
```python
|
||||
rule = (
|
||||
# ...the rest of the logic
|
||||
& Has("QoL item", options=[OptionFilter(Difficulty, Difficulty.option_easy)], filtered_resolution=True)
|
||||
)
|
||||
```
|
||||
|
||||
If you would like to provide option filters when reusing or composing rules, you can use the `Filtered` helper rule:
|
||||
|
||||
```python
|
||||
common_rule = Has("A") | HasAny("B", "C")
|
||||
...
|
||||
rule = (
|
||||
Filtered(common_rule, options=[OptionFilter(Opt, 0)]),
|
||||
| Filtered(Has("X") | CanReachRegion("Y"), options=[OptionFilter(Opt, 1)]),
|
||||
)
|
||||
```
|
||||
|
||||
You can also use the & and | operators to apply options to rules:
|
||||
|
||||
```python
|
||||
common_rule = Has("A")
|
||||
easy_filter = [OptionFilter(Difficulty, Difficulty.option_easy)]
|
||||
common_rule_only_on_easy = common_rule & easy_filter
|
||||
common_rule_skipped_on_easy = common_rule | easy_filter
|
||||
```
|
||||
|
||||
## Enabling caching
|
||||
|
||||
The rule builder provides a `CachedRuleBuilderWorld` base class for your `World` class that enables caching on your rules.
|
||||
|
||||
```python
|
||||
class MyWorld(CachedRuleBuilderWorld):
|
||||
game = "My Game"
|
||||
```
|
||||
|
||||
If your world's logic is very simple and you don't have many nested rules, the caching system may have more overhead cost than time it saves. You'll have to benchmark your own world to see if it should be enabled or not.
|
||||
|
||||
### Item name mapping
|
||||
|
||||
If you have multiple real items that map to a single logic item, add a `item_mapping` class dict to your world that maps actual item names to real item names so the cache system knows what to invalidate.
|
||||
|
||||
For example, if you have multiple `Currency x<num>` items on locations, but your rules only check a singular logical `Currency` item, eg `Has("Currency", 1000)`, you'll want to map each numerical currency item to the single logical `Currency`.
|
||||
|
||||
```python
|
||||
class MyWorld(CachedRuleBuilderWorld):
|
||||
item_mapping = {
|
||||
"Currency x10": "Currency",
|
||||
"Currency x50": "Currency",
|
||||
"Currency x100": "Currency",
|
||||
"Currency x500": "Currency",
|
||||
}
|
||||
```
|
||||
|
||||
## Defining custom rules
|
||||
|
||||
You can create a custom rule by creating a class that inherits from `Rule` or any of the default rules. You must provide the game name as an argument to the class. It's recommended to use the `@dataclass` decorator to reduce boilerplate, and to also provide your world as a type argument to add correct type checking to the `_instantiate` method.
|
||||
|
||||
You must provide or inherit a `Resolved` child class that defines an `_evaluate` method. This class will automatically be converted into a frozen `dataclass`. If your world has caching enabled you may need to define one or more dependencies functions as outlined below.
|
||||
|
||||
To add a rule that checks if the user has enough mcguffins to goal, with a randomized requirement:
|
||||
|
||||
```python
|
||||
@dataclasses.dataclass()
|
||||
class CanGoal(Rule["MyWorld"], game="My Game"):
|
||||
@override
|
||||
def _instantiate(self, world: "MyWorld") -> Rule.Resolved:
|
||||
# caching_enabled only needs to be passed in when your world inherits from CachedRuleBuilderWorld
|
||||
return self.Resolved(world.required_mcguffins, player=world.player, caching_enabled=True)
|
||||
|
||||
class Resolved(Rule.Resolved):
|
||||
goal: int
|
||||
|
||||
@override
|
||||
def _evaluate(self, state: CollectionState) -> bool:
|
||||
return state.has("McGuffin", self.player, count=self.goal)
|
||||
|
||||
@override
|
||||
def item_dependencies(self) -> dict[str, set[int]]:
|
||||
# this function is only required if you have caching enabled
|
||||
return {"McGuffin": {id(self)}}
|
||||
|
||||
@override
|
||||
def explain_json(self, state: CollectionState | None = None) -> list[JSONMessagePart]:
|
||||
# this method can be overridden to display custom explanations
|
||||
return [
|
||||
{"type": "text", "text": "Goal with "},
|
||||
{"type": "color", "color": "green" if state and self(state) else "salmon", "text": str(self.goal)},
|
||||
{"type": "text", "text": " McGuffins"},
|
||||
]
|
||||
```
|
||||
|
||||
Your custom rule can also resolve to builtin rules instead of needing to define your own:
|
||||
|
||||
```python
|
||||
@dataclasses.dataclass()
|
||||
class ComplicatedFilter(Rule["MyWorld"], game="My Game"):
|
||||
def _instantiate(self, world: "MyWorld") -> Rule.Resolved:
|
||||
if world.some_precalculated_bool:
|
||||
return Has("Item 1").resolve(world)
|
||||
if world.options.some_option:
|
||||
return CanReachRegion("Region 1").resolve(world)
|
||||
return False_().resolve(world)
|
||||
```
|
||||
|
||||
### Item dependencies
|
||||
|
||||
If your world inherits from `CachedRuleBuilderWorld` and there are items that when collected will affect the result of your rule evaluation, it must define an `item_dependencies` function that returns a mapping of the item name to the id of your rule. These dependencies will be combined to inform the caching system. It may be worthwhile to define this function even when caching is disabled as more things may use it in the future.
|
||||
|
||||
```python
|
||||
@dataclasses.dataclass()
|
||||
class MyRule(Rule["MyWorld"], game="My Game"):
|
||||
class Resolved(Rule.Resolved):
|
||||
item_name: str
|
||||
|
||||
@override
|
||||
def item_dependencies(self) -> dict[str, set[int]]:
|
||||
return {self.item_name: {id(self)}}
|
||||
```
|
||||
|
||||
All of the default `Has*` rules define this function already.
|
||||
|
||||
### Region dependencies
|
||||
|
||||
If your custom rule references other regions, it must define a `region_dependencies` function that returns a mapping of region names to the id of your rule regardless of if your world inherits from `CachedRuleBuilderWorld`. These dependencies will be combined to register indirect connections when you set this rule on an entrance and inform the caching system if applicable.
|
||||
|
||||
```python
|
||||
@dataclasses.dataclass()
|
||||
class MyRule(Rule["MyWorld"], game="My Game"):
|
||||
class Resolved(Rule.Resolved):
|
||||
region_name: str
|
||||
|
||||
@override
|
||||
def region_dependencies(self) -> dict[str, set[int]]:
|
||||
return {self.region_name: {id(self)}}
|
||||
```
|
||||
|
||||
The default `CanReachLocation`, `CanReachRegion`, and `CanReachEntrance` rules define this function already.
|
||||
|
||||
### Location dependencies
|
||||
|
||||
If your custom rule references other locations, it must define a `location_dependencies` function that returns a mapping of the location name to the id of your rule regardless of if your world inherits from `CachedRuleBuilderWorld`. These dependencies will be combined to register indirect connections when you set this rule on an entrance and inform the caching system if applicable.
|
||||
|
||||
```python
|
||||
@dataclasses.dataclass()
|
||||
class MyRule(Rule["MyWorld"], game="My Game"):
|
||||
class Resolved(Rule.Resolved):
|
||||
location_name: str
|
||||
|
||||
@override
|
||||
def location_dependencies(self) -> dict[str, set[int]]:
|
||||
return {self.location_name: {id(self)}}
|
||||
```
|
||||
|
||||
The default `CanReachLocation` rule defines this function already.
|
||||
|
||||
### Entrance dependencies
|
||||
|
||||
If your custom rule references other entrances, it must define a `entrance_dependencies` function that returns a mapping of the entrance name to the id of your rule regardless of if your world inherits from `CachedRuleBuilderWorld`. These dependencies will be combined to register indirect connections when you set this rule on an entrance and inform the caching system if applicable.
|
||||
|
||||
```python
|
||||
@dataclasses.dataclass()
|
||||
class MyRule(Rule["MyWorld"], game="My Game"):
|
||||
class Resolved(Rule.Resolved):
|
||||
entrance_name: str
|
||||
|
||||
@override
|
||||
def entrance_dependencies(self) -> dict[str, set[int]]:
|
||||
return {self.entrance_name: {id(self)}}
|
||||
```
|
||||
|
||||
The default `CanReachEntrance` rule defines this function already.
|
||||
|
||||
### Rule explanations
|
||||
|
||||
Resolved rules have a default implementation for `explain_json` and `explain_str` functions. The former optionally accepts a `CollectionState` and returns a list of `JSONMessagePart` appropriate for `print_json` in a client. It will display a human-readable message that explains what the rule requires. The latter is similar but returns a string. It is useful when debugging. There is also a `__str__` method defined to check what a rule is without a state.
|
||||
|
||||
To implement a custom message with a custom rule, override the `explain_json` and/or `explain_str` method on your `Resolved` class:
|
||||
|
||||
```python
|
||||
class MyRule(Rule, game="My Game"):
|
||||
class Resolved(Rule.Resolved):
|
||||
@override
|
||||
def explain_json(self, state: CollectionState | None = None) -> list[JSONMessagePart]:
|
||||
has_item = state and state.has("growth spurt", self.player)
|
||||
color = "yellow"
|
||||
start = "You must be "
|
||||
if has_item:
|
||||
start = "You are "
|
||||
color = "green"
|
||||
elif state is not None:
|
||||
start = "You are not "
|
||||
color = "salmon"
|
||||
return [
|
||||
{"type": "text", "text": start},
|
||||
{"type": "color", "color": color, "text": "THIS"},
|
||||
{"type": "text", "text": " tall to beat the game"},
|
||||
]
|
||||
|
||||
@override
|
||||
def explain_str(self, state: CollectionState | None = None) -> str:
|
||||
if state is None:
|
||||
return str(self)
|
||||
if state.has("growth spurt", self.player):
|
||||
return "You ARE this tall and can beat the game"
|
||||
return "You are not THIS tall and cannot beat the game"
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
return "You must be THIS tall to beat the game"
|
||||
```
|
||||
|
||||
### Cache control
|
||||
|
||||
By default your custom rule will work through the cache system as any other rule if caching is enabled. There are two class attributes on the `Resolved` class you can override to change this behavior.
|
||||
|
||||
- `force_recalculate`: Setting this to `True` will cause your custom rule to skip going through the caching system and always recalculate when being evaluated. When a rule with this flag enabled is composed with `And` or `Or` it will cause any parent rules to always force recalculate as well. Use this flag when it's difficult to determine when your rule should be marked as stale.
|
||||
- `skip_cache`: Setting this to `True` will also cause your custom rule to skip going through the caching system when being evaluated. However, it will **not** affect any other rules when composed with `And` or `Or`, so it must still define its `*_dependencies` functions as required. Use this flag when the evaluation of this rule is trivial and the overhead of the caching system will slow it down.
|
||||
|
||||
### Caveats
|
||||
|
||||
- Ensure you are passing `caching_enabled=True` in your `_instantiate` function when creating resolved rule instances if your world has opted into caching.
|
||||
- Resolved rules are forced to be frozen dataclasses. They and all their attributes must be immutable and hashable.
|
||||
- If your rule creates child rules ensure they are being resolved through the world rather than creating `Resolved` instances directly.
|
||||
|
||||
## Serialization
|
||||
|
||||
The rule builder is intended to be written first in Python for optimization and type safety. To facilitate exporting the rules to a client or tracker, rules have a `to_dict` method that returns a JSON-compatible dict. Since the location and entrance logic structure varies greatly from world to world, the actual JSON dumping is left up to the world dev.
|
||||
|
||||
The dict contains a `rule` key with the name of the rule, an `options` key with the rule's list of option filters, and an `args` key that contains any other arguments the individual rule has. For example, this is what a simple `Has` rule would look like:
|
||||
|
||||
```python
|
||||
{
|
||||
"rule": "Has",
|
||||
"options": [],
|
||||
"args": {
|
||||
"item_name": "Some item",
|
||||
"count": 1,
|
||||
},
|
||||
}
|
||||
```
|
||||
|
||||
For `And` and `Or` rules, instead of an `args` key, they have a `children` key containing a list of their child rules in the same serializable format:
|
||||
|
||||
```python
|
||||
{
|
||||
"rule": "And",
|
||||
"options": [],
|
||||
"children": [
|
||||
..., # each serialized rule
|
||||
]
|
||||
}
|
||||
```
|
||||
|
||||
A full example is as follows:
|
||||
|
||||
```python
|
||||
rule = And(
|
||||
Has("a", options=[OptionFilter(ToggleOption, 0)]),
|
||||
Or(Has("b", count=2), CanReachRegion("c"), options=[OptionFilter(ToggleOption, 1)]),
|
||||
)
|
||||
assert rule.to_dict() == {
|
||||
"rule": "And",
|
||||
"options": [],
|
||||
"children": [
|
||||
{
|
||||
"rule": "Has",
|
||||
"options": [
|
||||
{
|
||||
"option": "worlds.my_world.options.ToggleOption",
|
||||
"value": 0,
|
||||
"operator": "eq",
|
||||
},
|
||||
],
|
||||
"args": {
|
||||
"item_name": "a",
|
||||
"count": 1,
|
||||
},
|
||||
},
|
||||
{
|
||||
"rule": "Or",
|
||||
"options": [
|
||||
{
|
||||
"option": "worlds.my_world.options.ToggleOption",
|
||||
"value": 1,
|
||||
"operator": "eq",
|
||||
},
|
||||
],
|
||||
"children": [
|
||||
{
|
||||
"rule": "Has",
|
||||
"options": [],
|
||||
"args": {
|
||||
"item_name": "b",
|
||||
"count": 2,
|
||||
},
|
||||
},
|
||||
{
|
||||
"rule": "CanReachRegion",
|
||||
"options": [],
|
||||
"args": {
|
||||
"region_name": "c",
|
||||
},
|
||||
},
|
||||
],
|
||||
},
|
||||
],
|
||||
}
|
||||
```
|
||||
|
||||
### Custom serialization
|
||||
|
||||
To define a different format for your custom rules, override the `to_dict` function:
|
||||
|
||||
```python
|
||||
class BasicLogicRule(Rule, game="My Game"):
|
||||
items = ("one", "two")
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
# Return whatever format works best for you
|
||||
return {
|
||||
"logic": "basic",
|
||||
"items": self.items,
|
||||
}
|
||||
```
|
||||
|
||||
If your logic has been done in custom JSON first, you can define a `from_dict` class method on your rules to parse it correctly:
|
||||
|
||||
```python
|
||||
class BasicLogicRule(Rule, game="My Game"):
|
||||
@classmethod
|
||||
def from_dict(cls, data: Mapping[str, Any], world_cls: type[World]) -> Self:
|
||||
items = data.get("items", ())
|
||||
return cls(*items)
|
||||
```
|
||||
|
||||
## APIs
|
||||
|
||||
This section is provided for reference, refer to the above sections for examples.
|
||||
|
||||
### World API
|
||||
|
||||
These are properties and helpers that are available to you in your world.
|
||||
|
||||
#### Methods
|
||||
|
||||
- `rule_from_dict(data)`: Create a rule instance from a deserialized dict representation
|
||||
- `register_rule_builder_dependencies()`: Register all rules that depend on location or entrance access with the inherited dependencies, gets called automatically after set_rules
|
||||
- `set_rule(spot: Location | Entrance, rule: Rule)`: Resolve a rule, register its dependencies, and set it on the given location or entrance
|
||||
- `set_completion_rule(rule: Rule)`: Sets the completion condition for this world
|
||||
- `create_entrance(from_region: Region, to_region: Region, rule: Rule | None, name: str | None = None, force_creation: bool = False)`: Attempt to create an entrance from `from_region` to `to_region`, skipping creation if `rule` is defined and evaluates to `False_()` unless force_creation is `True`
|
||||
|
||||
#### CachedRuleBuilderWorld Properties
|
||||
|
||||
The following property is only available when inheriting from `CachedRuleBuilderWorld`
|
||||
|
||||
- `item_mapping: dict[str, str]`: A mapping of actual item name to logical item name
|
||||
|
||||
### Rule API
|
||||
|
||||
These are properties and helpers that you can use or override for custom rules.
|
||||
|
||||
- `_instantiate(world: World)`: Create a new resolved rule instance, override for custom rules as required
|
||||
- `to_dict()`: Create a JSON-compatible dict representation of this rule, override if you want to customize your rule's serialization
|
||||
- `from_dict(data, world_cls: type[World])`: Return a new rule instance from a deserialized representation, override if you've overridden `to_dict`
|
||||
- `__str__()`: Basic string representation of a rule, useful for debugging
|
||||
|
||||
#### Resolved rule API
|
||||
|
||||
- `player: int`: The slot this rule is resolved for
|
||||
- `_evaluate(state: CollectionState)`: Evaluate this rule against the given state, override this to define the logic for this rule
|
||||
- `item_dependencies()`: A mapping of item name to set of ids, override this if your custom rule depends on item collection
|
||||
- `region_dependencies()`: A mapping of region name to set of ids, override this if your custom rule depends on reaching regions
|
||||
- `location_dependencies()`: A mapping of location name to set of ids, override this if your custom rule depends on reaching locations
|
||||
- `entrance_dependencies()`: A mapping of entrance name to set of ids, override this if your custom rule depends on reaching entrances
|
||||
- `explain_json(state: CollectionState | None = None)`: Return a list of printJSON messages describing this rule's logic (and if state is defined its evaluation) in a human readable way, override to explain custom rules
|
||||
- `explain_str(state: CollectionState | None = None)`: Return a string describing this rule's logic (and if state is defined its evaluation) in a human readable way, override to explain custom rules, more useful for debugging
|
||||
- `__str__()`: A string describing this rule's logic without its evaluation, override to explain custom rules
|
||||
@@ -7,10 +7,9 @@ use that version. These steps are for developers or platforms without compiled r
|
||||
## General
|
||||
|
||||
What you'll need:
|
||||
* [Python 3.11.9 or newer](https://www.python.org/downloads/), not the Windows Store version
|
||||
* [Python 3.11.9 or newer but less than 3.14](https://www.python.org/downloads/), not the Windows Store version
|
||||
* On Windows, please consider only using the latest supported version in production environments since security
|
||||
updates for older versions are not easily available.
|
||||
* Python 3.13.x is currently the newest supported version
|
||||
* pip: included in downloads from python.org, separate in many Linux distributions
|
||||
* Matching C compiler
|
||||
* possibly optional, read operating system specific sections
|
||||
@@ -53,6 +52,32 @@ Recommended steps
|
||||
Refer to [Guide to Run Archipelago from Source Code on macOS](../worlds/generic/docs/mac_en.md).
|
||||
|
||||
|
||||
## Linux
|
||||
|
||||
If your Linux distribution ships a compatible Python version (see [General](#general)) and pip, you can use that,
|
||||
otherwise you may need to install Python from a 3rd party. Refer to documentation of your Linux distribution.
|
||||
|
||||
Installing a C compiler is usually optional. The package is typically named `gcc`, sometimes another package with the
|
||||
base build tools may be required, i.e. `build-essential` (Debian/Ubuntu) or `base-devel` (Arch).
|
||||
|
||||
After getting the source code, it is strongly recommended to create a
|
||||
[venv](https://docs.python.org/3/tutorial/venv.html) (Virtual Environment)
|
||||
by hand or using an IDE, such as PyCharm, because Archipelago requires specific versions of Python packages.
|
||||
|
||||
Run `python ModuleUpdate.py` in the project root to install packages, run `python Launcher.py` to run the Launcher.
|
||||
|
||||
### Building
|
||||
|
||||
Builds contain (almost) all dependencies to run Archipelago on any Linux distribution that is as new or newer than the
|
||||
one it was built on. Beware that currently only the oldest Ubuntu LTS available in GitHub actions is supported for that.
|
||||
This means the easiest way to generate a build is by running the `Build` action from GitHub actions instead of building
|
||||
locally. If you still want to, e.g. for local testing, you can by running
|
||||
|
||||
`python setup.py build_exe` to generate a binary distribution of Archipelago in `build/`. Or to generate an AppImage
|
||||
first generate the binary distribution and then run `python setup.py bdist_appimage` to populate `dist/`. You need to
|
||||
put an `appimagetool` into the directory you run the command from, rename it to `appimagetool` and make it executable.
|
||||
|
||||
|
||||
## Optional: A Link to the Past Enemizer
|
||||
|
||||
Only required to generate seeds that include A Link to the Past with certain options enabled. You will receive an
|
||||
|
||||
@@ -47,21 +47,27 @@
|
||||
|
||||
## HTML
|
||||
|
||||
* Indent with 2 spaces for new code.
|
||||
* Indent with 4 spaces for new code.
|
||||
* kebab-case for ids and classes.
|
||||
* Avoid using on* attributes (onclick, etc.).
|
||||
|
||||
## CSS
|
||||
## CSS / SCSS
|
||||
|
||||
* Indent with 2 spaces for new code.
|
||||
* Indent with 4 spaces for new code.
|
||||
* `{` on the same line as the selector.
|
||||
* No space between selector and `{`.
|
||||
* Space between selector and `{`.
|
||||
|
||||
## JS
|
||||
|
||||
* Indent with 2 spaces.
|
||||
* Indent `case` inside `switch ` with 2 spaces.
|
||||
* Use single quotes.
|
||||
* Indent with 4 spaces.
|
||||
* Indent `case` inside `switch ` with 4 spaces.
|
||||
* Prefer double quotation marks (`"`).
|
||||
* Semicolons are required after every statement.
|
||||
* Use [IIFEs](https://developer.mozilla.org/docs/Glossary/IIFE) to avoid polluting global scope.
|
||||
* Prefer to use [defer](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script#defer)
|
||||
in script tags, which retains order of execution but does not block.
|
||||
* Avoid `<script async ...` in most cases, see [async and defer](https://developer.mozilla.org/en-US/docs/Web/HTML/Reference/Elements/script#async_and_defer).
|
||||
* Use addEventListener.
|
||||
|
||||
## KV
|
||||
|
||||
|
||||
@@ -4,7 +4,7 @@ Archipelago has a rudimentary API that can be queried by endpoints. The API is a
|
||||
|
||||
The following API requests are formatted as: `https://<Archipelago URL>/api/<endpoint>`
|
||||
|
||||
The returned data will be formated in a combination of JSON lists or dicts, with their keys or values being notated in `blocks` (if applicable)
|
||||
The returned data will be formatted in a combination of JSON lists or dicts, with their keys or values being notated in `blocks` (if applicable)
|
||||
|
||||
Current endpoints:
|
||||
- Datapackage API
|
||||
@@ -24,13 +24,21 @@ Current endpoints:
|
||||
- [`/get_rooms`](#getrooms)
|
||||
- [`/get_seeds`](#getseeds)
|
||||
|
||||
## API Data Caching
|
||||
To reduce the strain on an Archipelago WebHost, many API endpoints will cache their data and only poll new data in timed intervals. Each endpoint has their own caching time related to the type of data being served. More dynamic data is refreshed more frequently, while static data is cached for longer.
|
||||
Each API endpoint will have their "Cache timer" listed under their definition (if any).
|
||||
API calls to these endpoints should not be faster than the listed timer. This will result in wasted processing for your client and (more importantly) the Archipelago WebHost, as the data will not be refreshed by the WebHost until the internal timer has elapsed.
|
||||
|
||||
|
||||
## Datapackage Endpoints
|
||||
These endpoints are used by applications to acquire a room's datapackage, and validate that they have the correct datapackage for use. Datapackages normally include, item IDs, location IDs, and name groupings, for a given room, and are essential for mapping IDs received from Archipelago to their correct items or locations.
|
||||
|
||||
### `/datapackage`
|
||||
<a name="datapackage"></a>
|
||||
|
||||
Fetches the current datapackage from the WebHost.
|
||||
**Cache timer: None**
|
||||
|
||||
You'll receive a dict named `games` that contains a named dict of every game and its data currently supported by Archipelago.
|
||||
Each game will have:
|
||||
- A checksum `checksum`
|
||||
@@ -40,7 +48,7 @@ Each game will have:
|
||||
- Location name to AP ID dict `location_name_to_id`
|
||||
|
||||
Example:
|
||||
```
|
||||
```json
|
||||
{
|
||||
"games": {
|
||||
...
|
||||
@@ -76,7 +84,10 @@ Example:
|
||||
|
||||
### `/datapackage/<string:checksum>`
|
||||
<a name="datapackagestringchecksum"></a>
|
||||
Fetches a single datapackage by checksum.
|
||||
|
||||
Fetches a single datapackage by checksum.
|
||||
**Cache timer: None**
|
||||
|
||||
Returns a dict of the game's data with:
|
||||
- A checksum `checksum`
|
||||
- A dict of item groups `item_name_groups`
|
||||
@@ -88,10 +99,13 @@ Its format will be identical to the whole-datapackage endpoint (`/datapackage`),
|
||||
|
||||
### `/datapackage_checksum`
|
||||
<a name="datapackagechecksum"></a>
|
||||
Fetches the checksums of the current static datapackages on the WebHost.
|
||||
|
||||
Fetches the checksums of the current static datapackages on the WebHost.
|
||||
**Cache timer: None**
|
||||
|
||||
You'll receive a dict with `game:checksum` key-value pairs for all the current officially supported games.
|
||||
Example:
|
||||
```
|
||||
```json
|
||||
{
|
||||
...
|
||||
"Donkey Kong Country 3":"f90acedcd958213f483a6a4c238e2a3faf92165e",
|
||||
@@ -108,6 +122,7 @@ These endpoints are used internally for the WebHost to generate games and valida
|
||||
<a name="generate"></a>
|
||||
Submits a game to the WebHost for generation.
|
||||
**This endpoint only accepts a POST HTTP request.**
|
||||
**Cache timer: None**
|
||||
|
||||
There are two ways to submit data for generation: With a file and with JSON.
|
||||
|
||||
@@ -116,7 +131,7 @@ Have your ZIP of yaml(s) or a single yaml, and submit a POST request to the `/ge
|
||||
If the options are valid, you'll be returned a successful generation response. (see [Generation Response](#generation-response))
|
||||
|
||||
Example using the python requests library:
|
||||
```
|
||||
```python
|
||||
file = {'file': open('Games.zip', 'rb')}
|
||||
req = requests.post("https://archipelago.gg/api/generate", files=file)
|
||||
```
|
||||
@@ -127,7 +142,7 @@ Finally, submit a POST request to the `/generate` endpoint.
|
||||
If the weighted options are valid, you'll be returned a successful generation response (see [Generation Response](#generation-response))
|
||||
|
||||
Example using the python requests library:
|
||||
```
|
||||
```python
|
||||
data = {"Test":{"game": "Factorio","name": "Test","Factorio": {}},}
|
||||
weights={"weights": data}
|
||||
req = requests.post("https://archipelago.gg/api/generate", json=weights)
|
||||
@@ -143,7 +158,7 @@ Upon successful generation, you'll be sent a JSON dict response detailing the ge
|
||||
- The API status page of the generation `wait_api_url` (see [Status Endpoint](#status))
|
||||
|
||||
Example:
|
||||
```
|
||||
```json
|
||||
{
|
||||
"detail": "19878f16-5a58-4b76-aab7-d6bf38be9463",
|
||||
"encoded": "GYePFlpYS3aqt9a_OL6UYw",
|
||||
@@ -167,12 +182,14 @@ If the generation detects a issue in generation, you'll be sent a dict with two
|
||||
- Detailed issue in `detail`
|
||||
|
||||
In the event of an unhandled server exception, you'll be provided a dict with a single key `text`:
|
||||
- Exception, `Uncought Exception: <error>` with a 500 status code
|
||||
- Exception, `Uncaught Exception: <error>` with a 500 status code
|
||||
|
||||
### `/status/<suuid:seed>`
|
||||
<a name="status"></a>
|
||||
Retrieves the status of the seed's generation.
|
||||
This endpoint will return a dict with a single key-vlaue pair. The key will always be `text`
|
||||
**Cache timer: None**
|
||||
|
||||
This endpoint will return a dict with a single key-value pair. The key will always be `text`
|
||||
The value will tell you the status of the generation:
|
||||
- Generation was completed: `Generation done` with a 201 status code
|
||||
- Generation request was not found: `Generation not found` with a 404 status code
|
||||
@@ -184,6 +201,8 @@ Endpoints to fetch information of the active WebHost room with the supplied room
|
||||
|
||||
### `/room_status/<suuid:room_id>`
|
||||
<a name="roomstatus"></a>
|
||||
**Cache timer: None**
|
||||
|
||||
Will provide a dict of room data with the following keys:
|
||||
- Tracker SUUID (`tracker`)
|
||||
- A list of players (`players`)
|
||||
@@ -192,10 +211,10 @@ Will provide a dict of room data with the following keys:
|
||||
- Last activity timestamp (`last_activity`)
|
||||
- The room timeout counter (`timeout`)
|
||||
- A list of downloads for files required for gameplay (`downloads`)
|
||||
- Each item is a dict containings the download URL and slot (`slot`, `download`)
|
||||
- Each item is a dict containing the download URL and slot (`slot`, `download`)
|
||||
|
||||
Example:
|
||||
```
|
||||
```json
|
||||
{
|
||||
"downloads": [
|
||||
{
|
||||
@@ -244,7 +263,7 @@ Example:
|
||||
]
|
||||
],
|
||||
"timeout": 7200,
|
||||
"tracker": "cf6989c0-4703-45d7-a317-2e5158431171"
|
||||
"tracker": "2gVkMQgISGScA8wsvDZg5A"
|
||||
}
|
||||
```
|
||||
|
||||
@@ -254,17 +273,27 @@ can either be viewed while on a room tracker page, or from the [room's endpoint]
|
||||
|
||||
### `/tracker/<suuid:tracker>`
|
||||
<a name=tracker></a>
|
||||
**Cache timer: 60 seconds**
|
||||
|
||||
Will provide a dict of tracker data with the following keys:
|
||||
|
||||
- Each player's current alias (`aliases`)
|
||||
- Will return the name if there is none
|
||||
- A list of items each player has received as a NetworkItem (`player_items_received`)
|
||||
- A list of players current alias data (`aliases`)
|
||||
- Each item containing a dict with, their alias `alias`, their player number `player`, and their team `team`
|
||||
- `alias` will return `null` if there is no alias set
|
||||
- A list of items each player has received as a [NetworkItem](network%20protocol.md#networkitem) (`player_items_received`)
|
||||
- Each item containing a dict with, a list of NetworkItems `items`, their player number `player`, their team `team`
|
||||
- A list of checks done by each player as a list of the location id's (`player_checks_done`)
|
||||
- The total number of checks done by all players (`total_checks_done`)
|
||||
- Hints that players have used or received (`hints`)
|
||||
- The time of last activity of each player in RFC 1123 format (`activity_timers`)
|
||||
- The time of last active connection of each player in RFC 1123 format (`connection_timers`)
|
||||
- The current client status of each player (`player_status`)
|
||||
- Each item containing a dict with, a list of checked location id's `locations`, their player number `player`, and their team `team`
|
||||
- A list of the total number of checks done by all players (`total_checks_done`)
|
||||
- Each item will contain a dict with, the total checks done `checks_done`, and the team `team`
|
||||
- A list of [Hints](network%20protocol.md#hint) data that players have used or received (`hints`)
|
||||
- Each item containing a dict containing, a list of hint data `hints`, the player number `player`, and their team `team`
|
||||
- A list containing the last activity time for each player, formatted in RFC 1123 format (`activity_timers`)
|
||||
- Each item containing, last activity time `time`, their player number `player`, and their team `team`
|
||||
- A list containing the last connection time for each player, formatted in RFC 1123 format (`connection_timers`)
|
||||
- Each item containing, the time of their last connection `time`, their player number `player`, and their team `team`
|
||||
- A list of the current [ClientStatus](network%20protocol.md#clientstatus) of each player (`player_status`)
|
||||
- Each item will contain, their status `status`, their player number `player`, and their team `team`
|
||||
|
||||
Example:
|
||||
```json
|
||||
@@ -279,7 +308,12 @@ Example:
|
||||
"team": 0,
|
||||
"player": 2,
|
||||
"alias": "Slot_Name_2"
|
||||
}
|
||||
},
|
||||
{
|
||||
"team": 0,
|
||||
"player": 3,
|
||||
"alias": null
|
||||
},
|
||||
],
|
||||
"player_items_received": [
|
||||
{
|
||||
@@ -378,12 +412,18 @@ Example:
|
||||
|
||||
### `/static_tracker/<suuid:tracker>`
|
||||
<a name=statictracker></a>
|
||||
**Cache timer: 300 seconds**
|
||||
|
||||
Will provide a dict of static tracker data with the following keys:
|
||||
|
||||
- item_link groups and their players (`groups`)
|
||||
- The datapackage hash for each game (`datapackage`)
|
||||
- A list of item_link groups and their member players (`groups`)
|
||||
- Each item containing a dict with, the slot registering the group `slot`, the item_link name `name`, and a list of members `members`
|
||||
- A dict of datapackage hashes for each game (`datapackage`)
|
||||
- Each item is a named dict of the game's name.
|
||||
- Each game contains two keys, the datapackage's checksum hash `checksum`, and the version `version`
|
||||
- This hash can then be sent to the datapackage API to receive the appropriate datapackage as necessary
|
||||
- The number of checks found vs. total checks available per player (`player_locations_total`)
|
||||
- A list of number of checks found vs. total checks available per player (`player_locations_total`)
|
||||
- Each list item contains a dict with three keys, the total locations for that slot `total_locations`, their player number `player`, and their team `team`
|
||||
- Same logic as the multitracker template: found = len(player_checks_done.locations) / total = player_locations_total.total_locations (all available checks).
|
||||
- The game each player is playing (`player_game`)
|
||||
- Provided as a list of objects with `team`, `player`, and `game`.
|
||||
@@ -446,7 +486,12 @@ Example:
|
||||
|
||||
### `/slot_data_tracker/<suuid:tracker>`
|
||||
<a name=slotdatatracker></a>
|
||||
Will provide a list of each player's slot_data.
|
||||
Will provide a list of each player's slot_data.
|
||||
**Cache timer: 300 seconds**
|
||||
|
||||
Each list item will contain a dict with the player's data:
|
||||
- player slot number `player`
|
||||
- A named dict `slot_data` containing any set slot data for that player
|
||||
|
||||
Example:
|
||||
```json
|
||||
@@ -474,6 +519,8 @@ User endpoints can get room and seed details from the current session tokens (co
|
||||
### `/get_rooms`
|
||||
<a name="getrooms"></a>
|
||||
Retreives a list of all rooms currently owned by the session token.
|
||||
**Cache timer: None**
|
||||
|
||||
Each list item will contain a dict with the room's details:
|
||||
- Room SUUID (`room_id`)
|
||||
- Seed SUUID (`seed_id`)
|
||||
@@ -484,25 +531,25 @@ Each list item will contain a dict with the room's details:
|
||||
- Room tracker SUUID (`tracker`)
|
||||
|
||||
Example:
|
||||
```
|
||||
```json
|
||||
[
|
||||
{
|
||||
"creation_time": "Fri, 18 Apr 2025 19:46:53 GMT",
|
||||
"last_activity": "Fri, 18 Apr 2025 21:16:02 GMT",
|
||||
"last_port": 52122,
|
||||
"room_id": "90ae5f9b-177c-4df8-ac53-9629fc3bff7a",
|
||||
"seed_id": "efbd62c2-aaeb-4dda-88c3-f461c029cef6",
|
||||
"room_id": "0D30FgQaRcWivFsw9o8qzw",
|
||||
"seed_id": "TFjiarBgTsCj5-Jbe8u33A",
|
||||
"timeout": 7200,
|
||||
"tracker": "cf6989c0-4703-45d7-a317-2e5158431171"
|
||||
"tracker": "52BycvJhRe6knrYH8v4bag"
|
||||
},
|
||||
{
|
||||
"creation_time": "Fri, 18 Apr 2025 20:36:42 GMT",
|
||||
"last_activity": "Fri, 18 Apr 2025 20:36:46 GMT",
|
||||
"last_port": 56884,
|
||||
"room_id": "14465c05-d08e-4d28-96bd-916f994609d8",
|
||||
"seed_id": "a528e34c-3b4f-42a9-9f8f-00a4fd40bacb",
|
||||
"room_id": "LMCFchESSNyuqcY3GxkhwA",
|
||||
"seed_id": "CENtJMXCTGmkIYCzjB5Csg",
|
||||
"timeout": 7200,
|
||||
"tracker": "4e624bd8-32b6-42e4-9178-aa407f72751c"
|
||||
"tracker": "2gVkMQgISGScA8wsvDZg5A"
|
||||
}
|
||||
]
|
||||
```
|
||||
@@ -510,6 +557,8 @@ Example:
|
||||
### `/get_seeds`
|
||||
<a name="getseeds"></a>
|
||||
Retreives a list of all seeds currently owned by the session token.
|
||||
**Cache timer: None**
|
||||
|
||||
Each item in the list will contain a dict with the seed's details:
|
||||
- Seed SUUID (`seed_id`)
|
||||
- Creation timestamp (`creation_time`)
|
||||
@@ -517,7 +566,7 @@ Each item in the list will contain a dict with the seed's details:
|
||||
- Each item in the list will contain a list of the slot name and game
|
||||
|
||||
Example:
|
||||
```
|
||||
```json
|
||||
[
|
||||
{
|
||||
"creation_time": "Fri, 18 Apr 2025 19:46:52 GMT",
|
||||
@@ -543,7 +592,7 @@ Example:
|
||||
"Ocarina of Time"
|
||||
]
|
||||
],
|
||||
"seed_id": "efbd62c2-aaeb-4dda-88c3-f461c029cef6"
|
||||
"seed_id": "CENtJMXCTGmkIYCzjB5Csg"
|
||||
},
|
||||
{
|
||||
"creation_time": "Fri, 18 Apr 2025 20:36:39 GMT",
|
||||
@@ -565,7 +614,7 @@ Example:
|
||||
"Archipelago"
|
||||
]
|
||||
],
|
||||
"seed_id": "a528e34c-3b4f-42a9-9f8f-00a4fd40bacb"
|
||||
"seed_id": "TFjiarBgTsCj5-Jbe8u33A"
|
||||
}
|
||||
]
|
||||
```
|
||||
@@ -225,7 +225,10 @@ and has a classification. The name needs to be unique within each game and must
|
||||
letter or symbol). The ID needs to be unique across all locations within the game.
|
||||
Locations and items can share IDs, and locations can share IDs with other games' locations.
|
||||
|
||||
World-specific IDs must be in the range 1 to 2<sup>53</sup>-1; IDs ≤ 0 are global and reserved.
|
||||
World-specific IDs **must** be in the range 1 to 2<sup>53</sup>-1 (the largest integer that is "[safe](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Number/MAX_SAFE_INTEGER#description)"
|
||||
to store in a 64-bit float, and thus all popular programming languages can handle). IDs ≤ 0 are global and reserved.
|
||||
It's **recommended** to keep your IDs in the range 1 to 2<sup>31</sup>-1,
|
||||
so only 32-bit integers are needed to hold your IDs.
|
||||
|
||||
Classification is one of `LocationProgressType.DEFAULT`, `PRIORITY` or `EXCLUDED`.
|
||||
The Fill algorithm will force progression items to be placed at priority locations, giving a higher chance of them being
|
||||
|
||||
@@ -208,6 +208,11 @@ Root: HKCR; Subkey: "{#MyAppName}apcivvipatch"; ValueData: "
|
||||
Root: HKCR; Subkey: "{#MyAppName}apcivvipatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLauncher.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}apcivvipatch\shell\open\command"; ValueData: """{app}\ArchipelagoLauncher.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".apeb"; ValueData: "{#MyAppName}ebpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}ebpatch"; ValueData: "Archipelago EarthBound Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}ebpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoSNIClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}ebpatch\shell\open\command"; ValueData: """{app}\ArchipelagoSNIClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".archipelago"; ValueData: "{#MyAppName}multidata"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}multidata"; ValueData: "Archipelago Server Data"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}multidata\DefaultIcon"; ValueData: "{app}\ArchipelagoServer.exe,0"; ValueType: string; ValueName: "";
|
||||
|
||||
12
kvui.py
12
kvui.py
@@ -19,6 +19,7 @@ os.environ["KIVY_NO_CONSOLELOG"] = "1"
|
||||
os.environ["KIVY_NO_FILELOG"] = "1"
|
||||
os.environ["KIVY_NO_ARGS"] = "1"
|
||||
os.environ["KIVY_LOG_ENABLE"] = "0"
|
||||
os.environ["SDL_MOUSE_FOCUS_CLICKTHROUGH"] = "1"
|
||||
|
||||
import Utils
|
||||
|
||||
@@ -35,6 +36,17 @@ Config.set("input", "mouse", "mouse,disable_multitouch")
|
||||
Config.set("kivy", "exit_on_escape", "0")
|
||||
Config.set("graphics", "multisamples", "0") # multisamples crash old intel drivers
|
||||
|
||||
# Workaround for Kivy issue #9226.
|
||||
# caused by kivy by default using probesysfs,
|
||||
# which assumes all multi touch deviecs are touch screens.
|
||||
# workaround provided by Snu of the kivy commmunity c:
|
||||
from kivy.utils import platform
|
||||
if platform == "linux":
|
||||
options = Config.options("input")
|
||||
for option in options:
|
||||
if Config.get("input", option) == "probesysfs":
|
||||
Config.remove_option("input", option)
|
||||
|
||||
# Workaround for an issue where importing kivy.core.window before loading sounds
|
||||
# will hang the whole application on Linux once the first sound is loaded.
|
||||
# kivymd imports kivy.core.window, so we have to do this before the first kivymd import.
|
||||
|
||||
@@ -13,5 +13,9 @@ cymem>=2.0.13
|
||||
orjson>=3.11.4
|
||||
typing_extensions>=4.15.0
|
||||
pyshortcuts>=1.9.6
|
||||
pathspec>=0.12.1
|
||||
kivymd @ git+https://github.com/kivymd/KivyMD@5ff9d0d
|
||||
kivymd>=2.0.1.dev0
|
||||
|
||||
# Legacy world dependencies that custom worlds rely on
|
||||
Pymem>=1.13.0
|
||||
|
||||
0
rule_builder/__init__.py
Normal file
0
rule_builder/__init__.py
Normal file
146
rule_builder/cached_world.py
Normal file
146
rule_builder/cached_world.py
Normal file
@@ -0,0 +1,146 @@
|
||||
from collections import defaultdict
|
||||
from typing import ClassVar, cast
|
||||
|
||||
from typing_extensions import override
|
||||
|
||||
from BaseClasses import CollectionState, Item, MultiWorld, Region
|
||||
from worlds.AutoWorld import LogicMixin, World
|
||||
|
||||
from .rules import Rule
|
||||
|
||||
|
||||
class CachedRuleBuilderWorld(World):
|
||||
"""A World subclass that provides helpers for interacting with the rule builder"""
|
||||
|
||||
rule_item_dependencies: dict[str, set[int]]
|
||||
"""A mapping of item name to set of rule ids"""
|
||||
|
||||
rule_region_dependencies: dict[str, set[int]]
|
||||
"""A mapping of region name to set of rule ids"""
|
||||
|
||||
rule_location_dependencies: dict[str, set[int]]
|
||||
"""A mapping of location name to set of rule ids"""
|
||||
|
||||
rule_entrance_dependencies: dict[str, set[int]]
|
||||
"""A mapping of entrance name to set of rule ids"""
|
||||
|
||||
item_mapping: ClassVar[dict[str, str]] = {}
|
||||
"""A mapping of actual item name to logical item name.
|
||||
Useful when there are multiple versions of a collected item but the logic only uses one. For example:
|
||||
item = Item("Currency x500"), rule = Has("Currency", count=1000), item_mapping = {"Currency x500": "Currency"}"""
|
||||
|
||||
rule_caching_enabled: ClassVar[bool] = True
|
||||
"""Flag to inform rules that the caching system for this world is enabled. It should not be overridden."""
|
||||
|
||||
def __init__(self, multiworld: MultiWorld, player: int) -> None:
|
||||
super().__init__(multiworld, player)
|
||||
self.rule_item_dependencies = defaultdict(set)
|
||||
self.rule_region_dependencies = defaultdict(set)
|
||||
self.rule_location_dependencies = defaultdict(set)
|
||||
self.rule_entrance_dependencies = defaultdict(set)
|
||||
|
||||
@override
|
||||
def register_rule_dependencies(self, resolved_rule: Rule.Resolved) -> None:
|
||||
for item_name, rule_ids in resolved_rule.item_dependencies().items():
|
||||
self.rule_item_dependencies[item_name] |= rule_ids
|
||||
for region_name, rule_ids in resolved_rule.region_dependencies().items():
|
||||
self.rule_region_dependencies[region_name] |= rule_ids
|
||||
for location_name, rule_ids in resolved_rule.location_dependencies().items():
|
||||
self.rule_location_dependencies[location_name] |= rule_ids
|
||||
for entrance_name, rule_ids in resolved_rule.entrance_dependencies().items():
|
||||
self.rule_entrance_dependencies[entrance_name] |= rule_ids
|
||||
|
||||
def register_rule_builder_dependencies(self) -> None:
|
||||
"""Register all rules that depend on locations or entrances with their dependencies"""
|
||||
for location_name, rule_ids in self.rule_location_dependencies.items():
|
||||
try:
|
||||
location = self.get_location(location_name)
|
||||
except KeyError:
|
||||
continue
|
||||
if not isinstance(location.access_rule, Rule.Resolved):
|
||||
continue
|
||||
for item_name in location.access_rule.item_dependencies():
|
||||
self.rule_item_dependencies[item_name] |= rule_ids
|
||||
for region_name in location.access_rule.region_dependencies():
|
||||
self.rule_region_dependencies[region_name] |= rule_ids
|
||||
|
||||
for entrance_name, rule_ids in self.rule_entrance_dependencies.items():
|
||||
try:
|
||||
entrance = self.get_entrance(entrance_name)
|
||||
except KeyError:
|
||||
continue
|
||||
if not isinstance(entrance.access_rule, Rule.Resolved):
|
||||
continue
|
||||
for item_name in entrance.access_rule.item_dependencies():
|
||||
self.rule_item_dependencies[item_name] |= rule_ids
|
||||
for region_name in entrance.access_rule.region_dependencies():
|
||||
self.rule_region_dependencies[region_name] |= rule_ids
|
||||
|
||||
@override
|
||||
def collect(self, state: CollectionState, item: Item) -> bool:
|
||||
changed = super().collect(state, item)
|
||||
if changed and self.rule_item_dependencies:
|
||||
player_results = cast(dict[int, bool], state.rule_builder_cache[self.player]) # pyright: ignore[reportAttributeAccessIssue, reportUnknownMemberType]
|
||||
mapped_name = self.item_mapping.get(item.name, "")
|
||||
rule_ids = self.rule_item_dependencies[item.name] | self.rule_item_dependencies[mapped_name]
|
||||
for rule_id in rule_ids:
|
||||
if player_results.get(rule_id, None) is False:
|
||||
del player_results[rule_id]
|
||||
|
||||
return changed
|
||||
|
||||
@override
|
||||
def remove(self, state: CollectionState, item: Item) -> bool:
|
||||
changed = super().remove(state, item)
|
||||
if not changed:
|
||||
return changed
|
||||
|
||||
player_results = cast(dict[int, bool], state.rule_builder_cache[self.player]) # pyright: ignore[reportAttributeAccessIssue, reportUnknownMemberType]
|
||||
if self.rule_item_dependencies:
|
||||
mapped_name = self.item_mapping.get(item.name, "")
|
||||
rule_ids = self.rule_item_dependencies[item.name] | self.rule_item_dependencies[mapped_name]
|
||||
for rule_id in rule_ids:
|
||||
player_results.pop(rule_id, None)
|
||||
|
||||
# clear all region dependent caches as none can be trusted
|
||||
if self.rule_region_dependencies:
|
||||
for rule_ids in self.rule_region_dependencies.values():
|
||||
for rule_id in rule_ids:
|
||||
player_results.pop(rule_id, None)
|
||||
|
||||
# clear all location dependent caches as they may have lost region access
|
||||
if self.rule_location_dependencies:
|
||||
for rule_ids in self.rule_location_dependencies.values():
|
||||
for rule_id in rule_ids:
|
||||
player_results.pop(rule_id, None)
|
||||
|
||||
# clear all entrance dependent caches as they may have lost region access
|
||||
if self.rule_entrance_dependencies:
|
||||
for rule_ids in self.rule_entrance_dependencies.values():
|
||||
for rule_id in rule_ids:
|
||||
player_results.pop(rule_id, None)
|
||||
|
||||
return changed
|
||||
|
||||
@override
|
||||
def reached_region(self, state: CollectionState, region: Region) -> None:
|
||||
super().reached_region(state, region)
|
||||
if self.rule_region_dependencies:
|
||||
player_results = cast(dict[int, bool], state.rule_builder_cache[self.player]) # pyright: ignore[reportAttributeAccessIssue, reportUnknownMemberType]
|
||||
for rule_id in self.rule_region_dependencies[region.name]:
|
||||
player_results.pop(rule_id, None)
|
||||
|
||||
|
||||
class CachedRuleBuilderLogicMixin(LogicMixin):
|
||||
multiworld: MultiWorld # pyright: ignore[reportUninitializedInstanceVariable]
|
||||
rule_builder_cache: dict[int, dict[int, bool]] # pyright: ignore[reportUninitializedInstanceVariable]
|
||||
|
||||
def init_mixin(self, multiworld: "MultiWorld") -> None:
|
||||
players = multiworld.get_all_ids()
|
||||
self.rule_builder_cache = {player: {} for player in players}
|
||||
|
||||
def copy_mixin(self, new_state: "CachedRuleBuilderLogicMixin") -> "CachedRuleBuilderLogicMixin":
|
||||
new_state.rule_builder_cache = {
|
||||
player: player_results.copy() for player, player_results in self.rule_builder_cache.items()
|
||||
}
|
||||
return new_state
|
||||
91
rule_builder/options.py
Normal file
91
rule_builder/options.py
Normal file
@@ -0,0 +1,91 @@
|
||||
import dataclasses
|
||||
import importlib
|
||||
import operator
|
||||
from collections.abc import Callable, Iterable
|
||||
from typing import Any, Final, Literal, Self, cast
|
||||
|
||||
from typing_extensions import override
|
||||
|
||||
from Options import CommonOptions, Option
|
||||
|
||||
Operator = Literal["eq", "ne", "gt", "lt", "ge", "le", "contains", "in"]
|
||||
|
||||
OPERATORS: Final[dict[Operator, Callable[..., bool]]] = {
|
||||
"eq": operator.eq,
|
||||
"ne": operator.ne,
|
||||
"gt": operator.gt,
|
||||
"lt": operator.lt,
|
||||
"ge": operator.ge,
|
||||
"le": operator.le,
|
||||
"contains": operator.contains,
|
||||
"in": operator.contains,
|
||||
}
|
||||
OPERATOR_STRINGS: Final[dict[Operator, str]] = {
|
||||
"eq": "==",
|
||||
"ne": "!=",
|
||||
"gt": ">",
|
||||
"lt": "<",
|
||||
"ge": ">=",
|
||||
"le": "<=",
|
||||
}
|
||||
REVERSE_OPERATORS: Final[tuple[Operator, ...]] = ("in",)
|
||||
|
||||
|
||||
@dataclasses.dataclass(frozen=True)
|
||||
class OptionFilter:
|
||||
option: type[Option[Any]]
|
||||
value: Any
|
||||
operator: Operator = "eq"
|
||||
|
||||
def to_dict(self) -> dict[str, Any]:
|
||||
"""Returns a JSON compatible dict representation of this option filter"""
|
||||
return {
|
||||
"option": f"{self.option.__module__}.{self.option.__name__}",
|
||||
"value": self.value,
|
||||
"operator": self.operator,
|
||||
}
|
||||
|
||||
def check(self, options: CommonOptions) -> bool:
|
||||
"""Tests the given options dataclass to see if it passes this option filter"""
|
||||
option_name = next(
|
||||
(name for name, cls in options.__class__.type_hints.items() if cls is self.option),
|
||||
None,
|
||||
)
|
||||
if option_name is None:
|
||||
raise ValueError(f"Cannot find option {self.option.__name__} in options class {options.__class__.__name__}")
|
||||
opt = cast(Option[Any] | None, getattr(options, option_name, None))
|
||||
if opt is None:
|
||||
raise ValueError(f"Invalid option: {option_name}")
|
||||
|
||||
fn = OPERATORS[self.operator]
|
||||
return fn(self.value, opt) if self.operator in REVERSE_OPERATORS else fn(opt, self.value)
|
||||
|
||||
@classmethod
|
||||
def from_dict(cls, data: dict[str, Any]) -> Self:
|
||||
"""Returns a new OptionFilter instance from a dict representation"""
|
||||
if "option" not in data or "value" not in data:
|
||||
raise ValueError("Missing required value and/or option")
|
||||
|
||||
option_path = data["option"]
|
||||
try:
|
||||
option_mod_name, option_cls_name = option_path.rsplit(".", 1)
|
||||
option_module = importlib.import_module(option_mod_name)
|
||||
option = getattr(option_module, option_cls_name, None)
|
||||
except (ValueError, ImportError) as e:
|
||||
raise ValueError(f"Cannot parse option '{option_path}'") from e
|
||||
if option is None or not issubclass(option, Option):
|
||||
raise ValueError(f"Invalid option '{option_path}' returns type '{option}' instead of Option subclass")
|
||||
|
||||
value = data["value"]
|
||||
operator = data.get("operator", "eq")
|
||||
return cls(option=cast(type[Option[Any]], option), value=value, operator=operator)
|
||||
|
||||
@classmethod
|
||||
def multiple_from_dict(cls, data: Iterable[dict[str, Any]]) -> tuple[Self, ...]:
|
||||
"""Returns a tuple of OptionFilters instances from an iterable of dict representations"""
|
||||
return tuple(cls.from_dict(o) for o in data)
|
||||
|
||||
@override
|
||||
def __str__(self) -> str:
|
||||
op = OPERATOR_STRINGS.get(self.operator, self.operator)
|
||||
return f"{self.option.__name__} {op} {self.value}"
|
||||
1822
rule_builder/rules.py
Normal file
1822
rule_builder/rules.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -1,9 +1,11 @@
|
||||
import unittest
|
||||
from typing import Callable, Dict, Optional
|
||||
from typing import Any, Dict, Optional
|
||||
|
||||
from typing_extensions import override
|
||||
|
||||
from BaseClasses import CollectionState, MultiWorld, Region
|
||||
from BaseClasses import CollectionRule, MultiWorld, Region
|
||||
from rule_builder.rules import Has, Rule
|
||||
from test.general import TestWorld
|
||||
|
||||
|
||||
class TestHelpers(unittest.TestCase):
|
||||
@@ -16,6 +18,7 @@ class TestHelpers(unittest.TestCase):
|
||||
self.multiworld.game[self.player] = "helper_test_game"
|
||||
self.multiworld.player_name = {1: "Tester"}
|
||||
self.multiworld.set_seed()
|
||||
self.multiworld.worlds[self.player] = TestWorld(self.multiworld, self.player)
|
||||
|
||||
def test_region_helpers(self) -> None:
|
||||
"""Tests `Region.add_locations()` and `Region.add_exits()` have correct behavior"""
|
||||
@@ -46,8 +49,9 @@ class TestHelpers(unittest.TestCase):
|
||||
"TestRegion1": {"TestRegion3"}
|
||||
}
|
||||
|
||||
exit_rules: Dict[str, Callable[[CollectionState], bool]] = {
|
||||
"TestRegion1": lambda state: state.has("test_item", self.player)
|
||||
exit_rules: Dict[str, CollectionRule | Rule[Any]] = {
|
||||
"TestRegion1": lambda state: state.has("test_item", self.player),
|
||||
"TestRegion2": Has("test_item2"),
|
||||
}
|
||||
|
||||
self.multiworld.regions += [Region(region, self.player, self.multiworld, regions[region]) for region in regions]
|
||||
@@ -74,13 +78,17 @@ class TestHelpers(unittest.TestCase):
|
||||
self.assertTrue(f"{parent} -> {exit_reg}" in created_exit_names)
|
||||
if exit_reg in exit_rules:
|
||||
entrance_name = exit_name if exit_name else f"{parent} -> {exit_reg}"
|
||||
self.assertEqual(exit_rules[exit_reg],
|
||||
self.multiworld.get_entrance(entrance_name, self.player).access_rule)
|
||||
rule = exit_rules[exit_reg]
|
||||
if isinstance(rule, Rule):
|
||||
self.assertEqual(rule.resolve(self.multiworld.worlds[self.player]),
|
||||
self.multiworld.get_entrance(entrance_name, self.player).access_rule)
|
||||
else:
|
||||
self.assertEqual(rule, self.multiworld.get_entrance(entrance_name, self.player).access_rule)
|
||||
|
||||
for region in reg_exit_set:
|
||||
for region, exit_set in reg_exit_set.items():
|
||||
current_region = self.multiworld.get_region(region, self.player)
|
||||
current_region.add_exits(reg_exit_set[region])
|
||||
current_region.add_exits(exit_set)
|
||||
exit_names = {_exit.name for _exit in current_region.exits}
|
||||
for reg_exit in reg_exit_set[region]:
|
||||
for reg_exit in exit_set:
|
||||
self.assertTrue(f"{region} -> {reg_exit}" in exit_names,
|
||||
f"{region} -> {reg_exit} not in {exit_names}")
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import unittest
|
||||
from argparse import Namespace
|
||||
from collections import ChainMap
|
||||
from typing import Type
|
||||
|
||||
from BaseClasses import CollectionState, MultiWorld
|
||||
@@ -82,12 +83,13 @@ class TestBase(unittest.TestCase):
|
||||
|
||||
def test_items_in_datapackage(self):
|
||||
"""Test that any created items in the itempool are in the datapackage"""
|
||||
archipelago = AutoWorldRegister.world_types["Archipelago"]
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
with self.subTest("Game", game=game_name):
|
||||
multiworld = setup_solo_multiworld(world_type)
|
||||
for item in multiworld.itempool:
|
||||
self.assertIn(item.name, world_type.item_name_to_id)
|
||||
|
||||
self.assertIn(item.name, ChainMap(world_type.item_name_to_id, archipelago.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.
|
||||
|
||||
@@ -1,8 +1,9 @@
|
||||
import unittest
|
||||
|
||||
from BaseClasses import PlandoOptions
|
||||
from Options import Choice, ItemLinks, PlandoConnections, PlandoItems, PlandoTexts
|
||||
from Options import Choice, ItemLinks, OptionSet, PlandoConnections, PlandoItems, PlandoTexts
|
||||
from Utils import restricted_dumps
|
||||
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
|
||||
|
||||
@@ -81,6 +82,19 @@ class TestOptions(unittest.TestCase):
|
||||
restricted_dumps(option.from_any(option.default))
|
||||
if issubclass(option, Choice) and option.default in option.name_lookup:
|
||||
restricted_dumps(option.from_text(option.name_lookup[option.default]))
|
||||
|
||||
def test_option_set_keys_random(self):
|
||||
"""Tests that option sets do not contain 'random' and its variants as valid keys"""
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
if game_name not in ("Archipelago", "Sudoku", "Super Metroid"):
|
||||
for option_key, option in world_type.options_dataclass.type_hints.items():
|
||||
if issubclass(option, OptionSet):
|
||||
with self.subTest(game=game_name, option=option_key):
|
||||
self.assertFalse(any(random_key in option.valid_keys for random_key in ("random",
|
||||
"random-high",
|
||||
"random-low")))
|
||||
for key in option.valid_keys:
|
||||
self.assertFalse("random-range" in key)
|
||||
|
||||
def test_pickle_dumps_plando(self):
|
||||
"""Test that plando options using containers of a custom type can be pickled"""
|
||||
|
||||
1336
test/general/test_rule_builder.py
Normal file
1336
test/general/test_rule_builder.py
Normal file
File diff suppressed because it is too large
Load Diff
@@ -25,31 +25,41 @@ class TestGenerateYamlTemplates(unittest.TestCase):
|
||||
if "World: with colon" in worlds.AutoWorld.AutoWorldRegister.world_types:
|
||||
del worlds.AutoWorld.AutoWorldRegister.world_types["World: with colon"]
|
||||
|
||||
|
||||
def test_name_with_colon(self) -> None:
|
||||
from Options import generate_yaml_templates
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
from worlds.AutoWorld import World
|
||||
from worlds.AutoWorld import World, WebWorld
|
||||
|
||||
class WebWorldWithColon(WebWorld):
|
||||
options_presets = {
|
||||
"Generic": {
|
||||
"progression_balancing": "disabled",
|
||||
"accessibility": "minimal",
|
||||
}
|
||||
}
|
||||
|
||||
class WorldWithColon(World):
|
||||
game = "World: with colon"
|
||||
item_name_to_id = {}
|
||||
location_name_to_id = {}
|
||||
web = WebWorldWithColon()
|
||||
|
||||
AutoWorldRegister.world_types = {WorldWithColon.game: WorldWithColon}
|
||||
with TemporaryDirectory(f"archipelago_{__name__}") as temp_dir:
|
||||
generate_yaml_templates(temp_dir)
|
||||
path: Path
|
||||
for path in Path(temp_dir).iterdir():
|
||||
self.assertTrue(path.is_file())
|
||||
self.assertTrue(path.suffix == ".yaml")
|
||||
with path.open(encoding="utf-8") as f:
|
||||
try:
|
||||
data = parse_yaml(f)
|
||||
except:
|
||||
f.seek(0)
|
||||
print(f"Error in {path.name}:\n{f.read()}")
|
||||
raise
|
||||
self.assertIn("game", data)
|
||||
self.assertIn(":", data["game"])
|
||||
self.assertIn(data["game"], data)
|
||||
self.assertIsInstance(data[data["game"]], dict)
|
||||
for path in Path(temp_dir).rglob("*"):
|
||||
if path.is_file():
|
||||
self.assertTrue(path.suffix == ".yaml")
|
||||
with path.open(encoding="utf-8") as f:
|
||||
try:
|
||||
data = parse_yaml(f)
|
||||
except:
|
||||
f.seek(0)
|
||||
print(f"Error in {path.name}:\n{f.read()}")
|
||||
raise
|
||||
self.assertIn("game", data)
|
||||
self.assertIn(":", data["game"])
|
||||
self.assertIn(data["game"], data)
|
||||
self.assertIsInstance(data[data["game"]], dict)
|
||||
|
||||
@@ -1,11 +1,22 @@
|
||||
import logging
|
||||
import os
|
||||
from uuid import UUID, uuid4, uuid5
|
||||
|
||||
from flask import url_for
|
||||
|
||||
from WebHostLib.customserver import set_up_logging, tear_down_logging
|
||||
from . import TestBase
|
||||
|
||||
|
||||
def _cleanup_logger(room_id: UUID) -> None:
|
||||
from Utils import user_path
|
||||
tear_down_logging(room_id)
|
||||
try:
|
||||
os.unlink(user_path("logs", f"{room_id}.txt"))
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
|
||||
class TestHostFakeRoom(TestBase):
|
||||
room_id: UUID
|
||||
log_filename: str
|
||||
@@ -39,7 +50,7 @@ class TestHostFakeRoom(TestBase):
|
||||
|
||||
try:
|
||||
os.unlink(self.log_filename)
|
||||
except FileNotFoundError:
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
def test_display_log_missing_full(self) -> None:
|
||||
@@ -191,3 +202,27 @@ class TestHostFakeRoom(TestBase):
|
||||
with db_session:
|
||||
commands = select(command for command in Command if command.room.id == self.room_id) # type: ignore
|
||||
self.assertNotIn("/help", (command.commandtext for command in commands))
|
||||
|
||||
def test_logger_teardown(self) -> None:
|
||||
"""Verify that room loggers are removed from the global logging manager."""
|
||||
from WebHostLib.customserver import tear_down_logging
|
||||
room_id = uuid4()
|
||||
self.addCleanup(_cleanup_logger, room_id)
|
||||
set_up_logging(room_id)
|
||||
self.assertIn(f"RoomLogger {room_id}", logging.Logger.manager.loggerDict)
|
||||
tear_down_logging(room_id)
|
||||
self.assertNotIn(f"RoomLogger {room_id}", logging.Logger.manager.loggerDict)
|
||||
|
||||
def test_handler_teardown(self) -> None:
|
||||
"""Verify that handlers for room loggers are closed by tear_down_logging."""
|
||||
from WebHostLib.customserver import tear_down_logging
|
||||
room_id = uuid4()
|
||||
self.addCleanup(_cleanup_logger, room_id)
|
||||
logger = set_up_logging(room_id)
|
||||
handlers = logger.handlers[:]
|
||||
self.assertGreater(len(handlers), 0)
|
||||
|
||||
tear_down_logging(room_id)
|
||||
for handler in handlers:
|
||||
if isinstance(handler, logging.FileHandler):
|
||||
self.assertTrue(handler.stream is None or handler.stream.closed)
|
||||
|
||||
@@ -2,7 +2,7 @@ import unittest
|
||||
|
||||
from BaseClasses import PlandoOptions
|
||||
from worlds import AutoWorldRegister
|
||||
from Options import OptionCounter, NamedRange, NumericOption, OptionList, OptionSet
|
||||
from Options import OptionCounter, NamedRange, NumericOption, OptionList, OptionSet, Visibility
|
||||
|
||||
|
||||
class TestOptionPresets(unittest.TestCase):
|
||||
@@ -19,6 +19,9 @@ class TestOptionPresets(unittest.TestCase):
|
||||
# pass in all plando options in case a preset wants to require certain plando options
|
||||
# for some reason
|
||||
option.verify(world_type, "Test Player", PlandoOptions(sum(PlandoOptions)))
|
||||
if not (Visibility.complex_ui in option.visibility or Visibility.simple_ui in option.visibility):
|
||||
self.fail(f"'{option_name}' in preset '{preset_name}' for game '{game_name}' is not "
|
||||
f"visible in any supported UI.")
|
||||
supported_types = [NumericOption, OptionSet, OptionList, OptionCounter]
|
||||
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}' "
|
||||
|
||||
82
typings/kivy/clock.pyi
Normal file
82
typings/kivy/clock.pyi
Normal file
@@ -0,0 +1,82 @@
|
||||
from _typeshed import Incomplete
|
||||
from kivy._clock import (
|
||||
ClockEvent as ClockEvent,
|
||||
ClockNotRunningError as ClockNotRunningError,
|
||||
CyClockBase as CyClockBase,
|
||||
CyClockBaseFree as CyClockBaseFree,
|
||||
FreeClockEvent as FreeClockEvent,
|
||||
)
|
||||
|
||||
__all__ = [
|
||||
"Clock",
|
||||
"ClockNotRunningError",
|
||||
"ClockEvent",
|
||||
"FreeClockEvent",
|
||||
"CyClockBase",
|
||||
"CyClockBaseFree",
|
||||
"triggered",
|
||||
"ClockBaseBehavior",
|
||||
"ClockBaseInterruptBehavior",
|
||||
"ClockBaseInterruptFreeBehavior",
|
||||
"ClockBase",
|
||||
"ClockBaseInterrupt",
|
||||
"ClockBaseFreeInterruptAll",
|
||||
"ClockBaseFreeInterruptOnly",
|
||||
"mainthread",
|
||||
]
|
||||
|
||||
class ClockBaseBehavior:
|
||||
MIN_SLEEP: float
|
||||
SLEEP_UNDERSHOOT: Incomplete
|
||||
def __init__(self, async_lib: str = "asyncio", **kwargs) -> None: ...
|
||||
def init_async_lib(self, lib) -> None: ...
|
||||
@property
|
||||
def frametime(self): ...
|
||||
@property
|
||||
def frames(self): ...
|
||||
@property
|
||||
def frames_displayed(self): ...
|
||||
def usleep(self, microseconds) -> None: ...
|
||||
def idle(self): ...
|
||||
async def async_idle(self): ...
|
||||
def tick(self) -> None: ...
|
||||
async def async_tick(self) -> None: ...
|
||||
def pre_idle(self) -> None: ...
|
||||
def post_idle(self, ts, current): ...
|
||||
def tick_draw(self) -> None: ...
|
||||
def get_fps(self): ...
|
||||
def get_rfps(self): ...
|
||||
def get_time(self): ...
|
||||
def get_boottime(self): ...
|
||||
time: Incomplete
|
||||
def handle_exception(self, e) -> None: ...
|
||||
|
||||
class ClockBaseInterruptBehavior(ClockBaseBehavior):
|
||||
interupt_next_only: bool
|
||||
def __init__(self, interupt_next_only: bool = False, **kwargs) -> None: ...
|
||||
def init_async_lib(self, lib) -> None: ...
|
||||
def usleep(self, microseconds) -> None: ...
|
||||
async def async_usleep(self, microseconds) -> None: ...
|
||||
def on_schedule(self, event) -> None: ...
|
||||
def idle(self): ...
|
||||
async def async_idle(self): ...
|
||||
|
||||
class ClockBaseInterruptFreeBehavior(ClockBaseInterruptBehavior):
|
||||
def __init__(self, **kwargs) -> None: ...
|
||||
def on_schedule(self, event): ...
|
||||
|
||||
class ClockBase(ClockBaseBehavior, CyClockBase):
|
||||
def __init__(self, **kwargs) -> None: ...
|
||||
def usleep(self, microseconds) -> None: ...
|
||||
|
||||
class ClockBaseInterrupt(ClockBaseInterruptBehavior, CyClockBase): ...
|
||||
class ClockBaseFreeInterruptAll(ClockBaseInterruptFreeBehavior, CyClockBaseFree): ...
|
||||
|
||||
class ClockBaseFreeInterruptOnly(ClockBaseInterruptFreeBehavior, CyClockBaseFree):
|
||||
def idle(self): ...
|
||||
async def async_idle(self): ...
|
||||
|
||||
def mainthread(func): ...
|
||||
def triggered(timeout: int = 0, interval: bool = False): ...
|
||||
|
||||
Clock: ClockBase
|
||||
@@ -5,17 +5,18 @@ import logging
|
||||
import pathlib
|
||||
import sys
|
||||
import time
|
||||
from collections.abc import Callable, Iterable, Mapping
|
||||
from random import Random
|
||||
from dataclasses import make_dataclass
|
||||
from typing import (Any, Callable, ClassVar, Dict, FrozenSet, Iterable, List, Mapping, Optional, Set, TextIO, Tuple,
|
||||
from typing import (Any, ClassVar, Dict, FrozenSet, List, Optional, Self, Set, TextIO, Tuple,
|
||||
TYPE_CHECKING, Type, Union)
|
||||
|
||||
from Options import item_and_loc_options, ItemsAccessibility, OptionGroup, PerGameCommonOptions
|
||||
from BaseClasses import CollectionState
|
||||
from BaseClasses import CollectionState, Entrance
|
||||
from rule_builder.rules import CustomRuleRegister, Rule
|
||||
from Utils import Version
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from BaseClasses import MultiWorld, Item, Location, Tutorial, Region, Entrance
|
||||
from BaseClasses import CollectionRule, Item, Location, MultiWorld, Region, Tutorial
|
||||
from NetUtils import GamesPackage, MultiData
|
||||
from settings import Group
|
||||
|
||||
@@ -47,27 +48,31 @@ class AutoWorldRegister(type):
|
||||
def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> AutoWorldRegister:
|
||||
if "web" in dct:
|
||||
assert isinstance(dct["web"], WebWorld), "WebWorld has to be instantiated."
|
||||
# filter out any events
|
||||
dct["item_name_to_id"] = {name: id for name, id in dct["item_name_to_id"].items() if id}
|
||||
dct["location_name_to_id"] = {name: id for name, id in dct["location_name_to_id"].items() if id}
|
||||
# build reverse lookups
|
||||
dct["item_id_to_name"] = {code: name for name, code in dct["item_name_to_id"].items()}
|
||||
dct["location_id_to_name"] = {code: name for name, code in dct["location_name_to_id"].items()}
|
||||
|
||||
# build rest
|
||||
dct["item_names"] = frozenset(dct["item_name_to_id"])
|
||||
dct["item_name_groups"] = {group_name: frozenset(group_set) for group_name, group_set
|
||||
in dct.get("item_name_groups", {}).items()}
|
||||
dct["item_name_groups"]["Everything"] = dct["item_names"]
|
||||
|
||||
dct["location_names"] = frozenset(dct["location_name_to_id"])
|
||||
dct["location_name_groups"] = {group_name: frozenset(group_set) for group_name, group_set
|
||||
in dct.get("location_name_groups", {}).items()}
|
||||
dct["location_name_groups"]["Everywhere"] = dct["location_names"]
|
||||
dct["all_item_and_group_names"] = frozenset(dct["item_names"] | set(dct.get("item_name_groups", {})))
|
||||
|
||||
# move away from get_required_client_version function
|
||||
if "game" in dct:
|
||||
assert "item_name_to_id" in dct, f"{name}: item_name_to_id is required"
|
||||
assert "location_name_to_id" in dct, f"{name}: location_name_to_id is required"
|
||||
|
||||
# filter out any events
|
||||
dct["item_name_to_id"] = {name: id for name, id in dct["item_name_to_id"].items() if id}
|
||||
dct["location_name_to_id"] = {name: id for name, id in dct["location_name_to_id"].items() if id}
|
||||
# build reverse lookups
|
||||
dct["item_id_to_name"] = {code: name for name, code in dct["item_name_to_id"].items()}
|
||||
dct["location_id_to_name"] = {code: name for name, code in dct["location_name_to_id"].items()}
|
||||
|
||||
# build rest
|
||||
dct["item_names"] = frozenset(dct["item_name_to_id"])
|
||||
dct["item_name_groups"] = {group_name: frozenset(group_set) for group_name, group_set
|
||||
in dct.get("item_name_groups", {}).items()}
|
||||
dct["item_name_groups"]["Everything"] = dct["item_names"]
|
||||
|
||||
dct["location_names"] = frozenset(dct["location_name_to_id"])
|
||||
dct["location_name_groups"] = {group_name: frozenset(group_set) for group_name, group_set
|
||||
in dct.get("location_name_groups", {}).items()}
|
||||
dct["location_name_groups"]["Everywhere"] = dct["location_names"]
|
||||
dct["all_item_and_group_names"] = frozenset(dct["item_names"] | set(dct.get("item_name_groups", {})))
|
||||
|
||||
# move away from get_required_client_version function
|
||||
assert "get_required_client_version" not in dct, f"{name}: required_client_version is an attribute now"
|
||||
# set minimum required_client_version from bases
|
||||
if "required_client_version" in dct and bases:
|
||||
@@ -173,7 +178,8 @@ def _timed_call(method: Callable[..., Any], *args: Any,
|
||||
|
||||
|
||||
def call_single(multiworld: "MultiWorld", method_name: str, player: int, *args: Any) -> Any:
|
||||
method = getattr(multiworld.worlds[player], method_name)
|
||||
world = multiworld.worlds[player]
|
||||
method = getattr(world, method_name)
|
||||
try:
|
||||
ret = _timed_call(method, *args, multiworld=multiworld, player=player)
|
||||
except Exception as e:
|
||||
@@ -184,6 +190,10 @@ def call_single(multiworld: "MultiWorld", method_name: str, player: int, *args:
|
||||
logging.error(message)
|
||||
raise e
|
||||
else:
|
||||
# Convenience for CachedRuleBuilderWorld users: Ensure that caching setup function is called
|
||||
# Can be removed once dependency system is improved
|
||||
if method_name == "set_rules" and hasattr(world, "register_rule_builder_dependencies"):
|
||||
call_single(multiworld, "register_rule_builder_dependencies", player)
|
||||
return ret
|
||||
|
||||
|
||||
@@ -484,7 +494,14 @@ class World(metaclass=AutoWorldRegister):
|
||||
raise NotImplementedError
|
||||
|
||||
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.
|
||||
|
||||
Any returned item name must be for a "repeatable" item, i.e. one that it's okay to generate arbitrarily many of.
|
||||
For most worlds this will be one or more of your filler items, but the classification of these items
|
||||
does not need to be ItemClassification.filler.
|
||||
The item name returned can be for a trap, useful, and/or progression item as long as it's repeatable.
|
||||
"""
|
||||
logging.warning(f"World {self} is generating a filler item without custom filler pool.")
|
||||
return self.random.choice(tuple(self.item_name_to_id.keys()))
|
||||
|
||||
@@ -538,6 +555,10 @@ class World(metaclass=AutoWorldRegister):
|
||||
return True
|
||||
return False
|
||||
|
||||
def reached_region(self, state: "CollectionState", region: "Region") -> None:
|
||||
"""Called when a region is newly reachable by the state."""
|
||||
pass
|
||||
|
||||
# following methods should not need to be overridden.
|
||||
def create_filler(self) -> "Item":
|
||||
return self.create_item(self.get_filler_item_name())
|
||||
@@ -586,6 +607,64 @@ class World(metaclass=AutoWorldRegister):
|
||||
res["checksum"] = data_package_checksum(res)
|
||||
return res
|
||||
|
||||
@classmethod
|
||||
def get_rule_cls(cls, name: str) -> type[Rule[Self]]:
|
||||
"""Returns the world-registered or default rule with the given name"""
|
||||
return CustomRuleRegister.get_rule_cls(cls.game, name)
|
||||
|
||||
@classmethod
|
||||
def rule_from_dict(cls, data: Mapping[str, Any]) -> Rule[Self]:
|
||||
"""Create a rule instance from a serialized dict representation"""
|
||||
name = data.get("rule", "")
|
||||
rule_class = cls.get_rule_cls(name)
|
||||
return rule_class.from_dict(data, cls)
|
||||
|
||||
def set_rule(self, spot: Location | Entrance, rule: CollectionRule | Rule[Any]) -> None:
|
||||
"""Sets an access rule for a location or entrance"""
|
||||
if isinstance(rule, Rule):
|
||||
rule = rule.resolve(self)
|
||||
self.register_rule_dependencies(rule)
|
||||
if isinstance(spot, Entrance):
|
||||
self._register_rule_indirects(rule, spot)
|
||||
spot.access_rule = rule
|
||||
|
||||
def set_completion_rule(self, rule: CollectionRule | Rule[Any]) -> None:
|
||||
"""Set the completion rule for this world"""
|
||||
if isinstance(rule, Rule):
|
||||
rule = rule.resolve(self)
|
||||
self.register_rule_dependencies(rule)
|
||||
self.multiworld.completion_condition[self.player] = rule
|
||||
|
||||
def create_entrance(
|
||||
self,
|
||||
from_region: Region,
|
||||
to_region: Region,
|
||||
rule: CollectionRule | Rule[Any] | None = None,
|
||||
name: str | None = None,
|
||||
force_creation: bool = False,
|
||||
) -> Entrance | None:
|
||||
"""Try to create an entrance between regions with the given rule,
|
||||
skipping it if the rule resolves to False (unless force_creation is True)"""
|
||||
if rule is not None and isinstance(rule, Rule):
|
||||
rule = rule.resolve(self)
|
||||
if rule.always_false and not force_creation:
|
||||
return None
|
||||
self.register_rule_dependencies(rule)
|
||||
|
||||
entrance = from_region.connect(to_region, name, rule=rule)
|
||||
if rule and isinstance(rule, Rule.Resolved):
|
||||
self._register_rule_indirects(rule, entrance)
|
||||
return entrance
|
||||
|
||||
def register_rule_dependencies(self, resolved_rule: Rule.Resolved) -> None:
|
||||
"""Hook for registering dependencies when a rule is assigned for this world"""
|
||||
pass
|
||||
|
||||
def _register_rule_indirects(self, resolved_rule: Rule.Resolved, entrance: Entrance) -> None:
|
||||
if self.explicit_indirect_conditions:
|
||||
for indirect_region in resolved_rule.region_dependencies().keys():
|
||||
self.multiworld.register_indirect_condition(self.get_region(indirect_region), entrance)
|
||||
|
||||
|
||||
# any methods attached to this can be used as part of CollectionState,
|
||||
# please use a prefix as all of them get clobbered together
|
||||
|
||||
@@ -5,7 +5,7 @@ import weakref
|
||||
from enum import Enum, auto
|
||||
from typing import Optional, Callable, List, Iterable, Tuple
|
||||
|
||||
from Utils import local_path, open_filename, is_frozen, is_kivy_running, open_file, user_path
|
||||
from Utils import local_path, open_filename, is_frozen, is_kivy_running, open_file, user_path, read_apignore
|
||||
|
||||
|
||||
class Type(Enum):
|
||||
@@ -247,7 +247,8 @@ components: List[Component] = [
|
||||
# MegaMan Battle Network 3
|
||||
Component('MMBN3 Client', 'MMBN3Client', file_identifier=SuffixIdentifier('.apbn3')),
|
||||
|
||||
Component("Export Datapackage", func=export_datapackage, component_type=Type.TOOL),
|
||||
Component("Export Datapackage", func=export_datapackage, component_type=Type.TOOL,
|
||||
description="Write item/location data for installed worlds to a file and open it."),
|
||||
]
|
||||
|
||||
|
||||
@@ -278,6 +279,10 @@ if not is_frozen():
|
||||
games = [(worldname, worldtype) for worldname, worldtype in AutoWorldRegister.world_types.items()
|
||||
if not worldtype.zip_path]
|
||||
|
||||
global_apignores = read_apignore(local_path("data", "GLOBAL.apignore"))
|
||||
if not global_apignores:
|
||||
raise RuntimeError("Could not read global apignore file for build component")
|
||||
|
||||
apworlds_folder = os.path.join("build", "apworlds")
|
||||
os.makedirs(apworlds_folder, exist_ok=True)
|
||||
for worldname, worldtype in games:
|
||||
@@ -305,18 +310,17 @@ if not is_frozen():
|
||||
apworld = APWorldContainer(str(zip_path))
|
||||
apworld.game = worldtype.game
|
||||
manifest.update(apworld.get_manifest())
|
||||
apworld.manifest_path = f"{file_name}/archipelago.json"
|
||||
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED,
|
||||
compresslevel=9) as zf:
|
||||
for path in pathlib.Path(world_directory).rglob("*"):
|
||||
relative_path = os.path.join(*path.parts[path.parts.index("worlds") + 1:])
|
||||
if "__MACOSX" in relative_path or ".DS_STORE" in relative_path or "__pycache__" in relative_path:
|
||||
continue
|
||||
if not relative_path.endswith("archipelago.json"):
|
||||
zf.write(path, relative_path)
|
||||
apworld.manifest_path = os.path.join(file_name, "archipelago.json")
|
||||
|
||||
local_ignores = read_apignore(pathlib.Path(world_directory, ".apignore"))
|
||||
apignores = global_apignores + local_ignores if local_ignores else global_apignores
|
||||
|
||||
with zipfile.ZipFile(zip_path, "w", zipfile.ZIP_DEFLATED, compresslevel=9) as zf:
|
||||
for file in apignores.match_tree_files(world_directory, negate=True):
|
||||
zf.write(pathlib.Path(world_directory, file), pathlib.Path(file_name, file))
|
||||
|
||||
zf.writestr(apworld.manifest_path, json.dumps(manifest))
|
||||
open_folder(apworlds_folder)
|
||||
|
||||
|
||||
components.append(Component("Build APWorlds", func=_build_apworlds, cli=True,
|
||||
description="Build APWorlds from loose-file world folders."))
|
||||
|
||||
@@ -1,17 +1,20 @@
|
||||
import importlib
|
||||
import importlib.util
|
||||
import importlib.abc
|
||||
import importlib.machinery
|
||||
import logging
|
||||
import os
|
||||
import sys
|
||||
import warnings
|
||||
import zipimport
|
||||
import time
|
||||
import dataclasses
|
||||
import json
|
||||
from typing import List
|
||||
from pathlib import Path
|
||||
from types import ModuleType
|
||||
from typing import List, Sequence
|
||||
from zipfile import BadZipFile
|
||||
|
||||
from NetUtils import DataPackage
|
||||
from Utils import local_path, user_path, Version, version_tuple, tuplize_version
|
||||
from Utils import local_path, user_path, Version, version_tuple, tuplize_version, messagebox
|
||||
|
||||
local_folder = os.path.dirname(__file__)
|
||||
user_folder = user_path("worlds") if user_path() != local_path() else user_path("custom_worlds")
|
||||
@@ -20,14 +23,14 @@ try:
|
||||
except OSError: # can't access/write?
|
||||
user_folder = None
|
||||
|
||||
__all__ = {
|
||||
__all__ = [
|
||||
"network_data_package",
|
||||
"AutoWorldRegister",
|
||||
"world_sources",
|
||||
"local_folder",
|
||||
"user_folder",
|
||||
"failed_world_loads",
|
||||
}
|
||||
]
|
||||
|
||||
|
||||
failed_world_loads: List[str] = []
|
||||
@@ -53,21 +56,7 @@ class WorldSource:
|
||||
def load(self) -> bool:
|
||||
try:
|
||||
start = time.perf_counter()
|
||||
if self.is_zip:
|
||||
importer = zipimport.zipimporter(self.resolved_path)
|
||||
spec = importer.find_spec(os.path.basename(self.path).rsplit(".", 1)[0])
|
||||
assert spec, f"{self.path} is not a loadable module"
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
|
||||
mod.__package__ = f"worlds.{mod.__package__}"
|
||||
|
||||
mod.__name__ = f"worlds.{mod.__name__}"
|
||||
sys.modules[mod.__name__] = mod
|
||||
with warnings.catch_warnings():
|
||||
warnings.filterwarnings("ignore", message="__package__ != __spec__.parent")
|
||||
importer.exec_module(mod)
|
||||
else:
|
||||
importlib.import_module(f".{self.path}", "worlds")
|
||||
importlib.import_module(f".{Path(self.path).stem}", "worlds")
|
||||
self.time_taken = time.perf_counter()-start
|
||||
return True
|
||||
|
||||
@@ -112,7 +101,6 @@ for world_source in world_sources:
|
||||
else:
|
||||
world_source.load()
|
||||
|
||||
|
||||
from .AutoWorld import AutoWorldRegister
|
||||
|
||||
for world_source in world_sources:
|
||||
@@ -157,6 +145,15 @@ if apworlds:
|
||||
logging.error(e)
|
||||
else:
|
||||
raise e
|
||||
except BadZipFile as e:
|
||||
err_message = (f"The world source {apworld_source.resolved_path} is not a valid zip. "
|
||||
"It is likely either corrupted, or was packaged incorrectly.")
|
||||
|
||||
if sys.stdout:
|
||||
raise RuntimeError(err_message) from e
|
||||
else:
|
||||
messagebox("Couldn't load worlds", err_message, error=True)
|
||||
sys.exit(1)
|
||||
|
||||
if apworld.minimum_ap_version and apworld.minimum_ap_version > version_tuple:
|
||||
fail_world(apworld.game,
|
||||
@@ -174,6 +171,16 @@ if apworlds:
|
||||
core_compatible.sort(
|
||||
key=lambda element: element[1].world_version if element[1].world_version else Version(0, 0, 0),
|
||||
reverse=True)
|
||||
|
||||
apworld_module_specs = {}
|
||||
class APWorldModuleFinder(importlib.abc.MetaPathFinder):
|
||||
def find_spec(
|
||||
self, fullname: str, _path: Sequence[str] | None, _target: ModuleType = None
|
||||
) -> importlib.machinery.ModuleSpec | None:
|
||||
return apworld_module_specs.get(fullname)
|
||||
|
||||
sys.meta_path.insert(0, APWorldModuleFinder())
|
||||
|
||||
for apworld_source, apworld in core_compatible:
|
||||
if apworld.game and apworld.game in AutoWorldRegister.world_types:
|
||||
fail_world(apworld.game,
|
||||
@@ -181,6 +188,12 @@ if apworlds:
|
||||
f"as its game {apworld.game} is already loaded.",
|
||||
add_as_failed_to_load=False)
|
||||
else:
|
||||
importer = zipimport.zipimporter(apworld_source.resolved_path)
|
||||
world_name = Path(apworld.path).stem
|
||||
|
||||
spec = importer.find_spec(f"worlds.{world_name}")
|
||||
apworld_module_specs[f"worlds.{world_name}"] = spec
|
||||
|
||||
apworld_source.load()
|
||||
if apworld.game in AutoWorldRegister.world_types:
|
||||
# world could fail to load at this point
|
||||
|
||||
@@ -2,4 +2,4 @@ mpyq>=0.2.5
|
||||
portpicker>=1.5.2
|
||||
aiohttp>=3.8.4
|
||||
loguru>=0.7.0
|
||||
protobuf==6.31.1
|
||||
protobuf==6.33.5
|
||||
|
||||
@@ -63,6 +63,9 @@ def is_location_valid(world: "HatInTimeWorld", location: str) -> bool:
|
||||
if not world.options.ShuffleStorybookPages and location in storybook_pages.keys():
|
||||
return False
|
||||
|
||||
if not world.options.ShuffleDirectorTokens and location in director_tokens.keys():
|
||||
return False
|
||||
|
||||
if not world.options.ShuffleActContracts and location in contract_locations.keys():
|
||||
return False
|
||||
|
||||
@@ -566,6 +569,34 @@ storybook_pages = {
|
||||
"Rumbi Factory - Page: Last Area": LocData(2000345883, "Time Rift - Rumbi Factory", dlc_flags=HatDLC.dlc2),
|
||||
}
|
||||
|
||||
director_tokens = {
|
||||
"Murder on the Owl Express - Conductor Token: Cafeteria": LocData(2001104767, "Murder on the Owl Express"),
|
||||
"Murder on the Owl Express - Conductor Token: Recreational Room": LocData(2001104768, "Murder on the Owl Express"),
|
||||
"Picture Perfect - DJ Grooves Token: Cardboard Puppy": LocData(2001203990, "Picture Perfect"),
|
||||
"Picture Perfect - DJ Grooves Token: Card Guessing Game": LocData(2001203991, "Picture Perfect"),
|
||||
"Picture Perfect - DJ Grooves Token: Back Alley": LocData(2001203992, "Picture Perfect"),
|
||||
"Picture Perfect - DJ Grooves Token: Cooking Show": LocData(2001203993, "Picture Perfect"),
|
||||
"Picture Perfect - DJ Grooves Token: Pon Cluster": LocData(2001203987, "Picture Perfect"),
|
||||
"Train Rush - Time Bonus: 1st Room": LocData(2001305235, "Train Rush", hookshot=True),
|
||||
"Train Rush - Time Bonus: Falling Platform": LocData(2001305189, "Train Rush", hookshot=True),
|
||||
"Train Rush - Time Bonus: Acid Crates": LocData(2001305186, "Train Rush", hookshot=True),
|
||||
"Train Rush - Time Bonus: Balloon": LocData(2001305239, "Train Rush", hookshot=True),
|
||||
"Train Rush - Time Bonus: Ring of Fire": LocData(2001305237, "Train Rush", hookshot=True),
|
||||
"Train Rush - Time Bonus: Blue Panels": LocData(2001305236, "Train Rush", hookshot=True),
|
||||
"Train Rush - Time Bonus: Sinking Lava Platform": LocData(2001305234, "Train Rush", hookshot=True),
|
||||
"Train Rush - Time Bonus: Lava Panels 1": LocData(2001305193, "Train Rush", hookshot=True),
|
||||
"Train Rush - Time Bonus: Lava Panels 2": LocData(2001305190, "Train Rush", hookshot=True),
|
||||
"Train Rush - Time Bonus: Lava Panels 3": LocData(2001305238, "Train Rush", hookshot=True),
|
||||
"The Big Parade - DJ Grooves Token (1/8)": LocData(2001400000, "The Big Parade"),
|
||||
"The Big Parade - DJ Grooves Token (2/8)": LocData(2001400001, "The Big Parade"),
|
||||
"The Big Parade - DJ Grooves Token (3/8)": LocData(2001400002, "The Big Parade"),
|
||||
"The Big Parade - DJ Grooves Token (4/8)": LocData(2001400003, "The Big Parade"),
|
||||
"The Big Parade - DJ Grooves Token (5/8)": LocData(2001400004, "The Big Parade", hit_type=HitType.umbrella),
|
||||
"The Big Parade - DJ Grooves Token (6/8)": LocData(2001400005, "The Big Parade", hit_type=HitType.umbrella),
|
||||
"The Big Parade - DJ Grooves Token (7/8)": LocData(2001400006, "The Big Parade", hit_type=HitType.umbrella),
|
||||
"The Big Parade - DJ Grooves Token (8/8)": LocData(2001400007, "The Big Parade", hit_type=HitType.umbrella),
|
||||
}
|
||||
|
||||
shop_locations = {
|
||||
"Badge Seller - Item 1": LocData(2000301003, "Badge Seller"),
|
||||
"Badge Seller - Item 2": LocData(2000301004, "Badge Seller"),
|
||||
@@ -1050,6 +1081,7 @@ location_table = {
|
||||
**ahit_locations,
|
||||
**act_completions,
|
||||
**storybook_pages,
|
||||
**director_tokens,
|
||||
**contract_locations,
|
||||
**shop_locations,
|
||||
}
|
||||
|
||||
@@ -72,6 +72,7 @@ def adjust_options(world: "HatInTimeWorld"):
|
||||
world.options.EndGoal.value = EndGoal.option_seal_the_deal
|
||||
world.options.ActRandomizer.value = 0
|
||||
world.options.ShuffleAlpineZiplines.value = 0
|
||||
world.options.ShuffleDirectorTokens.value = 0
|
||||
world.options.ShuffleSubconPaintings.value = 0
|
||||
world.options.ShuffleStorybookPages.value = 0
|
||||
world.options.ShuffleActContracts.value = 0
|
||||
@@ -219,6 +220,12 @@ class ShuffleStorybookPages(DefaultOnToggle):
|
||||
display_name = "Shuffle Storybook Pages"
|
||||
|
||||
|
||||
class ShuffleDirectorTokens(Toggle):
|
||||
"""If enabled, causes the Conductor/DJ Grooves tokens found in Chapter 2 levels to become item checks.
|
||||
NOTE: This also includes the time bonus pickups from Train Rush, since the level doesn't have any tokens."""
|
||||
display_name = "Shuffle Director Tokens"
|
||||
|
||||
|
||||
class ShuffleActContracts(DefaultOnToggle):
|
||||
"""If enabled, shuffle Snatcher's act contracts into the pool as items"""
|
||||
display_name = "Shuffle Contracts"
|
||||
@@ -658,6 +665,7 @@ class AHITOptions(PerGameCommonOptions):
|
||||
StartWithCompassBadge: StartWithCompassBadge
|
||||
CompassBadgeMode: CompassBadgeMode
|
||||
ShuffleStorybookPages: ShuffleStorybookPages
|
||||
ShuffleDirectorTokens: ShuffleDirectorTokens
|
||||
ShuffleActContracts: ShuffleActContracts
|
||||
ShuffleSubconPaintings: ShuffleSubconPaintings
|
||||
NoPaintingSkips: NoPaintingSkips
|
||||
@@ -722,7 +730,8 @@ class AHITOptions(PerGameCommonOptions):
|
||||
|
||||
|
||||
ahit_option_groups: Dict[str, List[Any]] = {
|
||||
"General Options": [EndGoal, ShuffleStorybookPages, ShuffleAlpineZiplines, ShuffleSubconPaintings,
|
||||
"General Options": [EndGoal, ShuffleStorybookPages, ShuffleDirectorTokens,
|
||||
ShuffleAlpineZiplines, ShuffleSubconPaintings,
|
||||
ShuffleActContracts, MinPonCost, MaxPonCost, BadgeSellerMinItems, BadgeSellerMaxItems,
|
||||
LogicDifficulty, NoPaintingSkips, CTRLogic],
|
||||
|
||||
@@ -759,6 +768,7 @@ slot_data_options: List[str] = [
|
||||
"StartWithCompassBadge",
|
||||
"CompassBadgeMode",
|
||||
"ShuffleStorybookPages",
|
||||
"ShuffleDirectorTokens",
|
||||
"ShuffleActContracts",
|
||||
"ShuffleSubconPaintings",
|
||||
"NoPaintingSkips",
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
from BaseClasses import Region, Entrance, ItemClassification, Location, LocationProgressType
|
||||
from .Types import ChapterIndex, Difficulty, HatInTimeLocation, HatInTimeItem
|
||||
from .Locations import location_table, storybook_pages, event_locs, is_location_valid, \
|
||||
shop_locations, TASKSANITY_START_ID, snatcher_coins, zero_jumps, zero_jumps_expert, zero_jumps_hard
|
||||
shop_locations, TASKSANITY_START_ID, snatcher_coins, zero_jumps, zero_jumps_expert, zero_jumps_hard, director_tokens
|
||||
from typing import TYPE_CHECKING, List, Dict, Optional
|
||||
from .Rules import set_rift_rules, get_difficulty
|
||||
from .Options import ActRandomizer, EndGoal
|
||||
@@ -859,6 +859,9 @@ def create_region(world: "HatInTimeWorld", name: str) -> Region:
|
||||
if key in storybook_pages.keys() and not world.options.ShuffleStorybookPages:
|
||||
continue
|
||||
|
||||
if key in director_tokens.keys() and not world.options.ShuffleDirectorTokens:
|
||||
continue
|
||||
|
||||
location = HatInTimeLocation(world.player, key, data.id, reg)
|
||||
reg.locations.append(location)
|
||||
if location.name in shop_locations:
|
||||
|
||||
@@ -16,10 +16,6 @@ def make_data_directory(dir_name: str) -> Path:
|
||||
gitignore = specific_data_directory / ".gitignore"
|
||||
|
||||
with open(gitignore, "w") as f:
|
||||
f.write(
|
||||
"""*
|
||||
!.gitignore
|
||||
"""
|
||||
)
|
||||
f.write("*\n")
|
||||
|
||||
return specific_data_directory
|
||||
|
||||
@@ -31,3 +31,21 @@ components.append(
|
||||
supports_uri=True,
|
||||
)
|
||||
)
|
||||
|
||||
# There are two optional parameters that are worth drawing attention to here: "game_name" and "supports_uri".
|
||||
# As you might know, on a room page on WebHost, clicking a slot name opens your locally installed Launcher
|
||||
# and asks you if you want to open a Text Client.
|
||||
# If you have "game_name" set on your Component, your user also gets the option to open that instead.
|
||||
# Furthermore, if you have "supports_uri" set to True, your Component will be passed a uri as an arg.
|
||||
# This uri contains the room url + port, the slot name, and the password.
|
||||
# You can process this uri arg to automatically connect the user to their slot without having to type anything.
|
||||
|
||||
# As you can see above, the APQuest client has both of these parameters set.
|
||||
# This means a user can click on the slot name of an APQuest slot on WebHost,
|
||||
# then click "APQuest Client" instead of "Text Client" in the Launcher popup, and after a few seconds,
|
||||
# they will be connected and playing the game without having to touch their keyboard once.
|
||||
|
||||
# Since a Component is just Python code, this doesn't just work with CommonClient-derived clients.
|
||||
# You could forward this uri arg to your standalone C++/Java/.NET/whatever client as well,
|
||||
# meaning just about every client can support this "Click on slot name -> Everything happens automatically" action.
|
||||
# The author would like to see more clients be aware of this feature and try to support it.
|
||||
|
||||
@@ -158,11 +158,11 @@ class Game:
|
||||
if not self.gameboard.ready:
|
||||
return
|
||||
|
||||
if self.active_math_problem is not None:
|
||||
if input_key in DIGIT_INPUTS_TO_DIGITS:
|
||||
self.math_problem_input(DIGIT_INPUTS_TO_DIGITS[input_key])
|
||||
if input_key == Input.BACKSPACE:
|
||||
self.math_problem_delete()
|
||||
if input_key in DIGIT_INPUTS_TO_DIGITS:
|
||||
self.math_problem_input(DIGIT_INPUTS_TO_DIGITS[input_key])
|
||||
return
|
||||
if input_key == Input.BACKSPACE:
|
||||
self.math_problem_delete()
|
||||
return
|
||||
|
||||
if input_key == Input.LEFT:
|
||||
|
||||
@@ -5,7 +5,8 @@ from typing import Any
|
||||
from worlds.AutoWorld import World
|
||||
|
||||
# Imports of your world's files must be relative.
|
||||
from . import items, locations, options, regions, rules, web_world
|
||||
from . import items, locations, regions, rules, web_world
|
||||
from . import options as apquest_options # rename due to a name conflict with World.options
|
||||
|
||||
# APQuest will go through all the parts of the world api one step at a time,
|
||||
# with many examples and comments across multiple files.
|
||||
@@ -36,8 +37,9 @@ class APQuestWorld(World):
|
||||
web = web_world.APQuestWebWorld()
|
||||
|
||||
# This is how we associate the options defined in our options.py with our world.
|
||||
options_dataclass = options.APQuestOptions
|
||||
options: options.APQuestOptions # Common mistake: This has to be a colon (:), not an equals sign (=).
|
||||
# (Note: options.py has been imported as "apquest_options" at the top of this file to avoid a name conflict)
|
||||
options_dataclass = apquest_options.APQuestOptions
|
||||
options: apquest_options.APQuestOptions # Common mistake: This has to be a colon (:), not an equals sign (=).
|
||||
|
||||
# Our world class must have a static location_name_to_id and item_name_to_id defined.
|
||||
# We define these in regions.py and items.py respectively, so we just set them here.
|
||||
|
||||
@@ -29,6 +29,7 @@ class ItemGroup(Enum):
|
||||
UTILITY = 4
|
||||
SONG = 5
|
||||
TURTLE = 6
|
||||
DOOR = 7
|
||||
|
||||
|
||||
class AquariaItem(Item):
|
||||
@@ -211,6 +212,7 @@ class ItemNames:
|
||||
TRANSTURTLE_BODY = "Transturtle Final Boss"
|
||||
TRANSTURTLE_SIMON_SAYS = "Transturtle Simon Says"
|
||||
TRANSTURTLE_ARNASSI_RUINS = "Transturtle Arnassi Ruins"
|
||||
DOOR_TO_CATHEDRAL = "Door to the Cathedral opened"
|
||||
# Events name
|
||||
BODY_TONGUE_CLEARED = "Body Tongue cleared"
|
||||
HAS_SUN_CRYSTAL = "Has Sun Crystal"
|
||||
@@ -240,7 +242,7 @@ item_table = {
|
||||
ItemNames.BIG_SEED: ItemData(698002, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_big_seed
|
||||
ItemNames.GLOWING_SEED: ItemData(698003, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_bio_seed
|
||||
ItemNames.BLACK_PEARL: ItemData(698004, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_blackpearl
|
||||
ItemNames.BABY_BLASTER: ItemData(698005, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_blaster
|
||||
ItemNames.BABY_BLASTER: ItemData(698005, 1, ItemType.PROGRESSION, ItemGroup.UTILITY), # collectible_blaster
|
||||
ItemNames.CRAB_ARMOR: ItemData(698006, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_crab_costume
|
||||
ItemNames.BABY_DUMBO: ItemData(698007, 1, ItemType.PROGRESSION, ItemGroup.UTILITY), # collectible_dumbo
|
||||
ItemNames.TOOTH: ItemData(698008, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_energy_boss
|
||||
@@ -256,8 +258,8 @@ item_table = {
|
||||
ItemNames.MITHALAS_BANNER: ItemData(698018, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithalas_banner
|
||||
ItemNames.MITHALAS_POT: ItemData(698019, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithalas_pot
|
||||
ItemNames.MUTANT_COSTUME: ItemData(698020, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mutant_costume
|
||||
ItemNames.BABY_NAUTILUS: ItemData(698021, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_nautilus
|
||||
ItemNames.BABY_PIRANHA: ItemData(698022, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_piranha
|
||||
ItemNames.BABY_NAUTILUS: ItemData(698021, 1, ItemType.PROGRESSION, ItemGroup.UTILITY), # collectible_nautilus
|
||||
ItemNames.BABY_PIRANHA: ItemData(698022, 1, ItemType.PROGRESSION, ItemGroup.UTILITY), # collectible_piranha
|
||||
ItemNames.ARNASSI_ARMOR: ItemData(698023, 1, ItemType.PROGRESSION, ItemGroup.UTILITY), # collectible_seahorse_costume
|
||||
ItemNames.SEED_BAG: ItemData(698024, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_seed_bag
|
||||
ItemNames.KING_S_SKULL: ItemData(698025, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_skull
|
||||
@@ -371,4 +373,20 @@ item_table = {
|
||||
ItemNames.TRANSTURTLE_BODY: ItemData(698131, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_finalboss
|
||||
ItemNames.TRANSTURTLE_SIMON_SAYS: ItemData(698132, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_forest05
|
||||
ItemNames.TRANSTURTLE_ARNASSI_RUINS: ItemData(698133, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_seahorse
|
||||
ItemNames.DOOR_TO_CATHEDRAL: ItemData(698134, 1, ItemType.PROGRESSION, ItemGroup.DOOR), # door_to_cathedral
|
||||
}
|
||||
|
||||
|
||||
four_gods_excludes = [ItemNames.ANEMONE, ItemNames.ARNASSI_STATUE, ItemNames.BIG_SEED, ItemNames.GLOWING_SEED,
|
||||
ItemNames.BLACK_PEARL, ItemNames.TOOTH, ItemNames.ENERGY_STATUE, ItemNames.KROTITE_ARMOR,
|
||||
ItemNames.GOLDEN_STARFISH, ItemNames.GOLDEN_GEAR, ItemNames.JELLY_BEACON,
|
||||
ItemNames.JELLY_PLANT, ItemNames.MITHALAS_DOLL, ItemNames.MITHALAN_DRESS,
|
||||
ItemNames.MITHALAS_BANNER, ItemNames.MITHALAS_POT, ItemNames.MUTANT_COSTUME, ItemNames.SEED_BAG,
|
||||
ItemNames.KING_S_SKULL, ItemNames.SONG_PLANT_SPORE, ItemNames.STONE_HEAD, ItemNames.SUN_KEY,
|
||||
ItemNames.GIRL_COSTUME, ItemNames.ODD_CONTAINER, ItemNames.TRIDENT, ItemNames.TURTLE_EGG,
|
||||
ItemNames.JELLY_EGG, ItemNames.URCHIN_COSTUME, ItemNames.BABY_WALKER,
|
||||
ItemNames.RAINBOW_MUSHROOM, ItemNames.RAINBOW_MUSHROOM, ItemNames.RAINBOW_MUSHROOM,
|
||||
ItemNames.LEAF_POULTICE, ItemNames.LEAF_POULTICE, ItemNames.LEAF_POULTICE,
|
||||
ItemNames.LEECHING_POULTICE, ItemNames.LEECHING_POULTICE, ItemNames.ARCANE_POULTICE,
|
||||
ItemNames.ROTTEN_MEAT, ItemNames.ROTTEN_MEAT, ItemNames.ROTTEN_MEAT, ItemNames.ROTTEN_MEAT,
|
||||
ItemNames.SEA_LOAF_X_2, ItemNames.SMALL_EGG]
|
||||
@@ -233,7 +233,7 @@ class AquariaLocationNames:
|
||||
SUN_TEMPLE_BULB_AT_THE_TOP_OF_THE_HIGH_DARK_ROOM = "Sun Temple, bulb at the top of the high dark room"
|
||||
SUN_TEMPLE_GOLDEN_GEAR = "Sun Temple, Golden Gear"
|
||||
SUN_TEMPLE_FIRST_BULB_OF_THE_TEMPLE = "Sun Temple, first bulb of the temple"
|
||||
SUN_TEMPLE_BULB_ON_THE_RIGHT_PART = "Sun Temple, bulb on the right part"
|
||||
SUN_TEMPLE_BULB_ON_THE_RIGHT_PART = "Sun Temple, bulb in the right part"
|
||||
SUN_TEMPLE_BULB_IN_THE_HIDDEN_ROOM_OF_THE_RIGHT_PART = "Sun Temple, bulb in the hidden room of the right part"
|
||||
SUN_TEMPLE_SUN_KEY = "Sun Temple, Sun Key"
|
||||
SUN_TEMPLE_BOSS_PATH_FIRST_PATH_BULB = "Sun Temple boss path, first path bulb"
|
||||
@@ -306,6 +306,7 @@ class AquariaLocationNames:
|
||||
BEATING_CRABBIUS_MAXIMUS = "Beating Crabbius Maximus"
|
||||
BEATING_MANTIS_SHRIMP_PRIME = "Beating Mantis Shrimp Prime"
|
||||
BEATING_KING_JELLYFISH_GOD_PRIME = "Beating King Jellyfish God Prime"
|
||||
SITTING_ON_THRONE = "Mithalas City Castle, sitting on the sealed throne"
|
||||
FIRST_SECRET = "First Secret"
|
||||
SECOND_SECRET = "Second Secret"
|
||||
THIRD_SECRET = "Third Secret"
|
||||
@@ -497,6 +498,7 @@ class AquariaLocations:
|
||||
locations_mithalas_castle = {
|
||||
AquariaLocationNames.MITHALAS_CITY_CASTLE_BULB_IN_THE_FLESH_HOLE: 698042,
|
||||
AquariaLocationNames.MITHALAS_CITY_CASTLE_BLUE_BANNER: 698165,
|
||||
AquariaLocationNames.SITTING_ON_THRONE: 698218,
|
||||
}
|
||||
|
||||
locations_mithalas_castle_urns = {
|
||||
@@ -803,6 +805,10 @@ class AquariaLocations:
|
||||
AquariaLocationNames.FINAL_BOSS_AREA_TRANSTURTLE: 698215,
|
||||
}
|
||||
|
||||
locations_final_boss_tube_transturtle_only = {
|
||||
AquariaLocationNames.FINAL_BOSS_AREA_TRANSTURTLE: 698215,
|
||||
}
|
||||
|
||||
locations_final_boss = {
|
||||
AquariaLocationNames.FINAL_BOSS_AREA_BULB_IN_THE_BOSS_THIRD_FORM_ROOM: 698106,
|
||||
}
|
||||
|
||||
@@ -11,9 +11,11 @@ from Options import Toggle, Choice, Range, PerGameCommonOptions, DefaultOnToggle
|
||||
class IngredientRandomizer(Choice):
|
||||
"""
|
||||
Select if the simple ingredients (that do not have a recipe) should be randomized.
|
||||
|
||||
If "Common Ingredients" is selected, the randomization will exclude the "Red Bulb", "Special Bulb" and "Rukh Egg".
|
||||
"""
|
||||
display_name = "Randomize Ingredients"
|
||||
rich_text_doc = True
|
||||
option_off = 0
|
||||
alias_false = 0
|
||||
option_common_ingredients = 1
|
||||
@@ -26,11 +28,17 @@ class IngredientRandomizer(Choice):
|
||||
class DishRandomizer(Toggle):
|
||||
"""Randomize the drop of Dishes (Ingredients with recipe)."""
|
||||
display_name = "Dish Randomizer"
|
||||
rich_text_doc = True
|
||||
|
||||
|
||||
class TurtleRandomizer(Choice):
|
||||
"""Randomize the transportation turtle."""
|
||||
"""
|
||||
Randomize the transportation turtle.
|
||||
|
||||
If the objective is "killing the four gods" or "Gods and Creator", the abyss and body turtle will not be randomized.
|
||||
"""
|
||||
display_name = "Turtle Randomizer"
|
||||
rich_text_doc = True
|
||||
option_none = 0
|
||||
alias_off = 0
|
||||
alias_false = 0
|
||||
@@ -47,6 +55,7 @@ class EarlyBindSong(Choice):
|
||||
selected).
|
||||
"""
|
||||
display_name = "Early Bind song"
|
||||
rich_text_doc = True
|
||||
option_off = 0
|
||||
alias_false = 0
|
||||
option_early = 1
|
||||
@@ -62,6 +71,7 @@ class EarlyEnergyForm(Choice):
|
||||
selected).
|
||||
"""
|
||||
display_name = "Early Energy form"
|
||||
rich_text_doc = True
|
||||
option_off = 0
|
||||
alias_false = 0
|
||||
option_early = 1
|
||||
@@ -74,14 +84,19 @@ class EarlyEnergyForm(Choice):
|
||||
class AquarianTranslation(Toggle):
|
||||
"""Translate the Aquarian scripture in the game into English."""
|
||||
display_name = "Translate Aquarian"
|
||||
rich_text_doc = True
|
||||
|
||||
|
||||
class BigBossesToBeat(Range):
|
||||
"""
|
||||
The number of big bosses to beat before having access to the creator (the final boss). The big bosses are
|
||||
"Fallen God", "Mithalan God", "Drunian God", "Lumerean God" and "The Golem".
|
||||
The number of big bosses to beat before having access to the creator (the final boss).
|
||||
|
||||
The big bosses are "Fallen God", "Mithalan God", "Drunian God", "Lumerean God" and "The Golem".
|
||||
|
||||
Has no effect if the objective is "killing the four gods" or "Gods and Creator".
|
||||
"""
|
||||
display_name = "Big bosses to beat"
|
||||
rich_text_doc = True
|
||||
range_start = 0
|
||||
range_end = 5
|
||||
default = 0
|
||||
@@ -89,12 +104,18 @@ class BigBossesToBeat(Range):
|
||||
|
||||
class MiniBossesToBeat(Range):
|
||||
"""
|
||||
The number of minibosses to beat before having access to the creator (the final boss). The minibosses are
|
||||
"Nautilus Prime", "Blaster Peg Prime", "Mergog", "Mithalan priests", "Octopus Prime", "Crabbius Maximus",
|
||||
"Mantis Shrimp Prime" and "King Jellyfish God Prime".
|
||||
Note that the Energy Statue and Simon Says are not minibosses.
|
||||
The number of minibosses to beat before having access to the goal.
|
||||
|
||||
The minibosses are "Nautilus Prime", "Blaster Peg Prime", "Mergog", "Mithalan priests", "Octopus Prime",
|
||||
"Crabbius Maximus", "Mantis Shrimp Prime" and "King Jellyfish God Prime".
|
||||
|
||||
Note that the "Energy Statue" and "Simon Says" are not minibosses.
|
||||
|
||||
Also note that if the objective is "killing the four enemy gods" or "Gods and creator", it might be needed to go in the abyss and
|
||||
bubble cave to kill "King Jellyfish God Prime" and "Mantis Shrimp Prime".
|
||||
"""
|
||||
display_name = "Minibosses to beat"
|
||||
rich_text_doc = True
|
||||
range_start = 0
|
||||
range_end = 8
|
||||
default = 0
|
||||
@@ -102,38 +123,48 @@ class MiniBossesToBeat(Range):
|
||||
|
||||
class Objective(Choice):
|
||||
"""
|
||||
The game objective can be to kill the creator or to kill the creator after obtaining all three secret memories.
|
||||
**Kill the Creator:** Get to the final boss (the Creator) and beat all it's forms.
|
||||
|
||||
**Obtain secrets and kill the Creator:** like the "Kill the Creator", but need to find all three secret memories
|
||||
before getting to the Creator.
|
||||
|
||||
**Killing the four gods:**, Beat all four enemy gods ("Fallen God", "Mithalan God", "Drunian God", "Lumerean God").
|
||||
|
||||
**Gods and Creator:** like "Killing the four gods" but you also have to beat the creator.
|
||||
"""
|
||||
display_name = "Objective"
|
||||
rich_text_doc = True
|
||||
option_kill_the_creator = 0
|
||||
option_obtain_secrets_and_kill_the_creator = 1
|
||||
option_killing_the_four_gods = 2
|
||||
option_gods_and_creator = 3
|
||||
default = 0
|
||||
|
||||
|
||||
class SkipFirstVision(Toggle):
|
||||
"""
|
||||
The first vision in the game, where Naija transforms into Energy Form and gets flooded by enemies, is quite cool but
|
||||
can be quite long when you already know what is going on. This option can be used to skip this vision.
|
||||
Skip the first vision in the game, where Naija transforms into Energy Form and gets flooded by enemies.
|
||||
"""
|
||||
display_name = "Skip Naija's first vision"
|
||||
rich_text_doc = True
|
||||
|
||||
|
||||
class NoProgressionHardOrHiddenLocation(Toggle):
|
||||
"""
|
||||
Make sure that there are no progression items at hard-to-reach or hard-to-find locations.
|
||||
Those locations are very High locations (that need beast form, soup and skill to get),
|
||||
every location in the bubble cave, locations where need you to cross a false wall without any indication,
|
||||
the Arnassi race, bosses and minibosses. Useful for those that want a more casual run.
|
||||
"""
|
||||
display_name = "No progression in hard or hidden locations"
|
||||
|
||||
|
||||
class LightNeededToGetToDarkPlaces(DefaultOnToggle):
|
||||
class LightNeededToGetToDarkPlaces(Choice):
|
||||
"""
|
||||
Make sure that the sun form or the dumbo pet can be acquired before getting to dark places.
|
||||
|
||||
Be aware that navigating in dark places without light is extremely difficult.
|
||||
|
||||
You can also force the sun form to be accessible by using the "sun form" option.
|
||||
"""
|
||||
display_name = "Light needed to get to dark places"
|
||||
rich_text_doc = True
|
||||
option_off = 0
|
||||
alias_false = 0
|
||||
option_on = 1
|
||||
alias_true = 1
|
||||
option_sun_form = 2
|
||||
default = 1
|
||||
|
||||
|
||||
class BindSongNeededToGetUnderRockBulb(DefaultOnToggle):
|
||||
@@ -141,23 +172,83 @@ class BindSongNeededToGetUnderRockBulb(DefaultOnToggle):
|
||||
Make sure that the bind song can be acquired before having to obtain sing bulbs under rocks.
|
||||
"""
|
||||
display_name = "Bind song needed to get sing bulbs under rocks"
|
||||
rich_text_doc = True
|
||||
|
||||
|
||||
class BlindGoal(Toggle):
|
||||
"""
|
||||
Hide the goal's requirements from the help page so that you have to go to the last boss door to know
|
||||
what is needed to access the boss.
|
||||
Hide the goal's requirements from the help page so that you don't know what is needed to goal.
|
||||
|
||||
Note that when you get to the final boss door (or you beat the last gods when the "Killing the four gods"
|
||||
is selected) you can then see the requirements in the help page.
|
||||
"""
|
||||
display_name = "Hide the goal's requirements"
|
||||
rich_text_doc = True
|
||||
|
||||
|
||||
class InfiniteHotSoup(DefaultOnToggle):
|
||||
"""
|
||||
As soon as a "hot soup" is received, the user will never run out of this dish.
|
||||
|
||||
This option is recommended if using Ingredient randomization since "hot soup" ingredients may become hard to get
|
||||
and the "hot soup" is necessary to get to some locations.
|
||||
"""
|
||||
display_name = "Infinite Hot Soup"
|
||||
rich_text_doc = True
|
||||
|
||||
|
||||
class SaveHealing(DefaultOnToggle):
|
||||
"""
|
||||
When you save, Naija is healed back to full health. If disabled, saving won't heal Naija.
|
||||
|
||||
Note that Naija can still heal by sleeping in some beds in the game (including in her home).
|
||||
"""
|
||||
display_name = "Save heal Naija"
|
||||
rich_text_doc = True
|
||||
|
||||
|
||||
class OpenBodyTongue(Toggle):
|
||||
"""
|
||||
Remove the body tongue making the body accessible without going in the sunken city
|
||||
"""
|
||||
display_name = "Open the body tongue"
|
||||
rich_text_doc = True
|
||||
|
||||
|
||||
class SkipFinalBoss3rdForm(Toggle):
|
||||
"""
|
||||
The Final boss third form (the hide and seek form) can be easy and quite long. So, this option can be used
|
||||
to skip this form.
|
||||
|
||||
Note that you will still need to deliver the final blow to the 3rd form in order to activate the 4th form animation.
|
||||
"""
|
||||
display_name = "Skip final boss third form"
|
||||
|
||||
|
||||
class MaximumIngredientAmount(Range):
|
||||
"""
|
||||
The maximum number of the same ingredients that can be stacked on the ingredient inventory.
|
||||
"""
|
||||
display_name = "Maximum ingredient amount"
|
||||
rich_text_doc = True
|
||||
range_start = 2
|
||||
range_end = 20
|
||||
default = 8
|
||||
|
||||
|
||||
class UnconfineHomeWater(Choice):
|
||||
"""
|
||||
Open the way out of the Home Waters area so that Naija can go to open water and beyond without the bind song.
|
||||
|
||||
**Via energy door:** Open the energy door between the home waters and the open waters
|
||||
|
||||
**Via transturtle:** Remove the rock blocking the home water transturtle.
|
||||
|
||||
Note that if you turn this option off, it is recommended to turn on the Early Energy form and Early Bind Song
|
||||
options.
|
||||
"""
|
||||
display_name = "Unconfine Home Waters Area"
|
||||
rich_text_doc = True
|
||||
option_off = 0
|
||||
alias_false = 0
|
||||
option_via_energy_door = 1
|
||||
@@ -168,6 +259,134 @@ class UnconfineHomeWater(Choice):
|
||||
default = 0
|
||||
|
||||
|
||||
class ThroneAsLocation(Toggle):
|
||||
"""
|
||||
If enabled, sitting on the Mithalas City Castle throne (with the seal on it) will be a location and opening the
|
||||
door to the Mithalas Cathedral will be an item.
|
||||
"""
|
||||
display_name = "Throne as a location"
|
||||
rich_text_doc = True
|
||||
|
||||
|
||||
class NoProgressionHardOrHiddenLocation(Toggle):
|
||||
"""
|
||||
Make sure that there are no progression items at hard-to-reach or hard-to-find locations.
|
||||
|
||||
Those locations are very High locations (that need beast form, soup and skill to get), every location in the
|
||||
bubble cave, locations where need you to cross a false wall without any indication, the Arnassi race,
|
||||
bosses and minibosses.
|
||||
|
||||
Useful for those that want a more casual run.
|
||||
"""
|
||||
display_name = "No progression in hard or hidden locations"
|
||||
rich_text_doc = True
|
||||
|
||||
|
||||
class NoProgressionSimonSays(Toggle):
|
||||
"""
|
||||
Make sure that there are no progression items in the says area.
|
||||
"""
|
||||
display_name = "No progression in Simon says area"
|
||||
rich_text_doc = True
|
||||
|
||||
|
||||
class NoProgressionKelpForest(Toggle):
|
||||
"""
|
||||
Make sure that there are no progression items in Kelp Forest (excluding Simon says area).
|
||||
|
||||
Can be useful to get smaller runs.
|
||||
"""
|
||||
display_name = "No progression in Kelp Forest"
|
||||
|
||||
|
||||
class NoProgressionVeil(Toggle):
|
||||
"""
|
||||
Make sure that there are no progression items in the Veil.
|
||||
|
||||
Can be useful to get smaller runs.
|
||||
"""
|
||||
display_name = "No progression in the Veil"
|
||||
rich_text_doc = True
|
||||
|
||||
|
||||
class NoProgressionMithalas(Toggle):
|
||||
"""
|
||||
Make sure that there are no progression items in the Mithalas (city, castle and cathedral).
|
||||
|
||||
Can be useful to get smaller runs.
|
||||
"""
|
||||
display_name = "No progression in Mithalas"
|
||||
rich_text_doc = True
|
||||
|
||||
|
||||
class NoProgressionEnergyTemple(Toggle):
|
||||
"""
|
||||
Make sure that there are no progression items in the Energy Temple.
|
||||
|
||||
Can be useful to get smaller runs.
|
||||
"""
|
||||
display_name = "No progression in the Energy Temple"
|
||||
rich_text_doc = True
|
||||
|
||||
|
||||
class NoProgressionArnassiRuins(Toggle):
|
||||
"""
|
||||
Make sure that there are no progression items in the Arnassi Ruins.
|
||||
|
||||
Can be useful to get smaller runs.
|
||||
|
||||
Note that if the Transportation turtle are not randomize, this include Simon Says area.
|
||||
"""
|
||||
display_name = "No progression in Arnassi Ruins"
|
||||
rich_text_doc = True
|
||||
|
||||
|
||||
class NoProgressionFrozenVeil(Toggle):
|
||||
"""
|
||||
Make sure that there are no progression items in the Frozen Veil (including Ice Cavern and Bubble Cave).
|
||||
|
||||
Can be useful to get smaller runs.
|
||||
"""
|
||||
display_name = "No progression in the Frozen Veil"
|
||||
rich_text_doc = True
|
||||
|
||||
|
||||
class NoProgressionAbyss(Toggle):
|
||||
"""
|
||||
Make sure that there are no progression items in the Abyss.
|
||||
|
||||
Can be useful to get smaller runs.
|
||||
|
||||
Has no effect if the objective is "killing the four gods".
|
||||
"""
|
||||
display_name = "No progression in the Abyss"
|
||||
rich_text_doc = True
|
||||
|
||||
|
||||
class NoProgressionSunkenCity(Toggle):
|
||||
"""
|
||||
Make sure that there are no progression items in the Sunken City.
|
||||
|
||||
Can be useful to get smaller runs.
|
||||
|
||||
Has no effect if the objective is "killing the four gods".
|
||||
"""
|
||||
display_name = "No progression in the Sunken City"
|
||||
rich_text_doc = True
|
||||
|
||||
|
||||
class NoProgressionBody(Toggle):
|
||||
"""
|
||||
Make sure that there are no progression items in the Body (including the before-boss transturtle room
|
||||
and the boss location).
|
||||
|
||||
Can be useful to get smaller runs.
|
||||
|
||||
Has no effect if the objective is "killing the four gods".
|
||||
"""
|
||||
display_name = "No progression in the Body"
|
||||
rich_text_doc = True
|
||||
|
||||
@dataclass
|
||||
class AquariaOptions(PerGameCommonOptions):
|
||||
"""
|
||||
@@ -183,9 +402,25 @@ class AquariaOptions(PerGameCommonOptions):
|
||||
light_needed_to_get_to_dark_places: LightNeededToGetToDarkPlaces
|
||||
bind_song_needed_to_get_under_rock_bulb: BindSongNeededToGetUnderRockBulb
|
||||
unconfine_home_water: UnconfineHomeWater
|
||||
no_progression_hard_or_hidden_locations: NoProgressionHardOrHiddenLocation
|
||||
ingredient_randomizer: IngredientRandomizer
|
||||
dish_randomizer: DishRandomizer
|
||||
aquarian_translation: AquarianTranslation
|
||||
skip_first_vision: SkipFirstVision
|
||||
blind_goal: BlindGoal
|
||||
infinite_hot_soup: InfiniteHotSoup
|
||||
open_body_tongue: OpenBodyTongue
|
||||
maximum_ingredient_amount: MaximumIngredientAmount
|
||||
skip_final_boss_3rd_form: SkipFinalBoss3rdForm
|
||||
save_healing: SaveHealing
|
||||
throne_as_location: ThroneAsLocation
|
||||
no_progression_hard_or_hidden_locations: NoProgressionHardOrHiddenLocation
|
||||
no_progression_simon_says: NoProgressionSimonSays
|
||||
no_progression_kelp_forest: NoProgressionKelpForest
|
||||
no_progression_veil: NoProgressionVeil
|
||||
no_progression_mithalas: NoProgressionMithalas
|
||||
no_progression_energy_temple: NoProgressionEnergyTemple
|
||||
no_progression_arnassi_ruins: NoProgressionArnassiRuins
|
||||
no_progression_frozen_veil: NoProgressionFrozenVeil
|
||||
no_progression_abyss: NoProgressionAbyss
|
||||
no_progression_sunken_city: NoProgressionSunkenCity
|
||||
no_progression_body: NoProgressionBody
|
||||
|
||||
@@ -8,7 +8,7 @@ from typing import Dict, Optional, Iterable
|
||||
from BaseClasses import MultiWorld, Region, Entrance, Item, ItemClassification, CollectionState
|
||||
from .Items import AquariaItem, ItemNames
|
||||
from .Locations import AquariaLocations, AquariaLocation, AquariaLocationNames
|
||||
from .Options import AquariaOptions, UnconfineHomeWater
|
||||
from .Options import AquariaOptions, UnconfineHomeWater, LightNeededToGetToDarkPlaces, Objective
|
||||
from worlds.generic.Rules import add_rule, set_rule
|
||||
|
||||
|
||||
@@ -116,14 +116,27 @@ def _has_big_bosses(state: CollectionState, player: int) -> bool:
|
||||
ItemNames.LUMEREAN_GOD_BEATED, ItemNames.THE_GOLEM_BEATED}, player)
|
||||
|
||||
|
||||
def _has_mini_bosses(state: CollectionState, player: int) -> bool:
|
||||
def _has_four_gods_beated(state: CollectionState, player: int) -> bool:
|
||||
"""`player` in `state` has beated every big bosses"""
|
||||
return state.has_all({ItemNames.FALLEN_GOD_BEATED, ItemNames.MITHALAN_GOD_BEATED, ItemNames.DRUNIAN_GOD_BEATED,
|
||||
ItemNames.LUMEREAN_GOD_BEATED}, player)
|
||||
|
||||
|
||||
def _has_mini_bosses(state: CollectionState, player: int) -> bool:
|
||||
"""`player` in `state` has beated every mini bosses"""
|
||||
return state.has_all({ItemNames.NAUTILUS_PRIME_BEATED, ItemNames.BLASTER_PEG_PRIME_BEATED, ItemNames.MERGOG_BEATED,
|
||||
ItemNames.MITHALAN_PRIESTS_BEATED, ItemNames.OCTOPUS_PRIME_BEATED,
|
||||
ItemNames.CRABBIUS_MAXIMUS_BEATED, ItemNames.MANTIS_SHRIMP_PRIME_BEATED,
|
||||
ItemNames.KING_JELLYFISH_GOD_PRIME_BEATED}, player)
|
||||
|
||||
|
||||
def _has_mini_bosses_four_gods(state: CollectionState, player: int) -> bool:
|
||||
"""`player` in `state` has beated every mini bosses other than the ones in the abyss and in the bubble cave"""
|
||||
return state.has_all({ItemNames.NAUTILUS_PRIME_BEATED, ItemNames.BLASTER_PEG_PRIME_BEATED, ItemNames.MERGOG_BEATED,
|
||||
ItemNames.MITHALAN_PRIESTS_BEATED, ItemNames.OCTOPUS_PRIME_BEATED,
|
||||
ItemNames.CRABBIUS_MAXIMUS_BEATED}, player)
|
||||
|
||||
|
||||
def _has_secrets(state: CollectionState, player: int) -> bool:
|
||||
"""The secrets have been acquired in the `state` of the `player`"""
|
||||
return state.has_all({ItemNames.FIRST_SECRET_OBTAINED, ItemNames.SECOND_SECRET_OBTAINED,
|
||||
@@ -133,6 +146,11 @@ def _item_not_advancement(item: Item):
|
||||
"""The `item` is not an advancement item"""
|
||||
return not item.advancement
|
||||
|
||||
def _is_cathedral_door_opened(state: CollectionState, player: int) -> bool:
|
||||
"""The door to Mithalas Cathedral has been opened in the `state` of the `player`"""
|
||||
return state.has(ItemNames.DOOR_TO_CATHEDRAL, player)
|
||||
|
||||
|
||||
class AquariaRegions:
|
||||
"""
|
||||
Class used to create regions of the Aquaria game
|
||||
@@ -236,10 +254,11 @@ class AquariaRegions:
|
||||
body_rt: Region
|
||||
body_rb: Region
|
||||
body_b: Region
|
||||
final_boss_loby: Region
|
||||
final_boss_lobby: Region
|
||||
final_boss_tube: Region
|
||||
final_boss: Region
|
||||
final_boss_end: Region
|
||||
four_gods_end: Region
|
||||
"""
|
||||
Every Region of the game
|
||||
"""
|
||||
@@ -254,6 +273,11 @@ class AquariaRegions:
|
||||
The ID of the player
|
||||
"""
|
||||
|
||||
is_four_gods: bool
|
||||
"""
|
||||
True if the player has an objective that implies killing the four gods
|
||||
"""
|
||||
|
||||
def __add_region(self, hint: str,
|
||||
locations: Optional[Dict[str, int]]) -> Region:
|
||||
"""
|
||||
@@ -439,65 +463,82 @@ class AquariaRegions:
|
||||
self.sun_temple_boss = self.__add_region("Sun Temple boss area",
|
||||
AquariaLocations.locations_sun_temple_boss)
|
||||
|
||||
def __create_abyss(self) -> None:
|
||||
def __create_abyss(self, add_locations: bool) -> None:
|
||||
"""
|
||||
Create the `abyss_*`, `ice_cave`, `king_jellyfish_cave` and `whale`
|
||||
regions
|
||||
"""
|
||||
self.abyss_l = self.__add_region("Abyss left area",
|
||||
AquariaLocations.locations_abyss_l)
|
||||
self.abyss_lb = self.__add_region("Abyss left bottom area", AquariaLocations.locations_abyss_lb)
|
||||
self.abyss_r = self.__add_region("Abyss right area", AquariaLocations.locations_abyss_r)
|
||||
AquariaLocations.locations_abyss_l if add_locations else None)
|
||||
self.abyss_lb = self.__add_region("Abyss left bottom area",
|
||||
AquariaLocations.locations_abyss_lb if add_locations else None)
|
||||
self.abyss_r = self.__add_region("Abyss right area",
|
||||
AquariaLocations.locations_abyss_r if add_locations else None)
|
||||
self.abyss_r_transturtle = self.__add_region("Abyss right area, transturtle",
|
||||
AquariaLocations.locations_abyss_r_transturtle)
|
||||
self.abyss_r_whale = self.__add_region("Abyss right area, outside the whale",
|
||||
AquariaLocations.locations_abyss_r_whale)
|
||||
self.ice_cave = self.__add_region("Ice Cavern", AquariaLocations.locations_ice_cave)
|
||||
AquariaLocations.locations_abyss_r_whale if add_locations else None)
|
||||
self.ice_cave = self.__add_region("Ice Cavern",
|
||||
AquariaLocations.locations_ice_cave if add_locations else None)
|
||||
self.frozen_feil = self.__add_region("Frozen Veil", None)
|
||||
self.bubble_cave = self.__add_region("Bubble Cave", AquariaLocations.locations_bubble_cave)
|
||||
self.bubble_cave_boss = self.__add_region("Bubble Cave boss area", AquariaLocations.locations_bubble_cave_boss)
|
||||
self.bubble_cave = self.__add_region("Bubble Cave",
|
||||
AquariaLocations.locations_bubble_cave if add_locations else None)
|
||||
self.bubble_cave_boss = self.__add_region("Bubble Cave boss area",
|
||||
AquariaLocations.locations_bubble_cave_boss
|
||||
if add_locations else None)
|
||||
self.king_jellyfish_cave = self.__add_region("Abyss left area, King jellyfish cave",
|
||||
AquariaLocations.locations_king_jellyfish_cave)
|
||||
self.whale = self.__add_region("Inside the whale", AquariaLocations.locations_whale)
|
||||
AquariaLocations.locations_king_jellyfish_cave
|
||||
if add_locations else None)
|
||||
self.whale = self.__add_region("Inside the whale",
|
||||
AquariaLocations.locations_whale if add_locations else None)
|
||||
|
||||
self.first_secret = self.__add_region("First Secret area", None)
|
||||
|
||||
def __create_sunken_city(self) -> None:
|
||||
def __create_sunken_city(self, add_locations: bool) -> None:
|
||||
"""
|
||||
Create the `sunken_city_*` regions
|
||||
"""
|
||||
self.sunken_city_l = self.__add_region("Sunken City left area", None)
|
||||
self.sunken_city_l_crates = self.__add_region("Sunken City left area",
|
||||
AquariaLocations.locations_sunken_city_l)
|
||||
AquariaLocations.locations_sunken_city_l
|
||||
if add_locations else None)
|
||||
self.sunken_city_l_bedroom = self.__add_region("Sunken City left area, bedroom",
|
||||
AquariaLocations.locations_sunken_city_l_bedroom)
|
||||
AquariaLocations.locations_sunken_city_l_bedroom
|
||||
if add_locations else None)
|
||||
self.sunken_city_r = self.__add_region("Sunken City right area", None)
|
||||
self.sunken_city_r_crates = self.__add_region("Sunken City right area crates",
|
||||
AquariaLocations.locations_sunken_city_r)
|
||||
AquariaLocations.locations_sunken_city_r
|
||||
if add_locations else None)
|
||||
self.sunken_city_boss = self.__add_region("Sunken City boss area",
|
||||
AquariaLocations.locations_sunken_city_boss)
|
||||
AquariaLocations.locations_sunken_city_boss
|
||||
if add_locations else None)
|
||||
|
||||
def __create_body(self) -> None:
|
||||
def __create_body(self, add_locations: bool) -> None:
|
||||
"""
|
||||
Create the `body_*` and `final_boss* regions
|
||||
"""
|
||||
self.body_c = self.__add_region("The Body center area",
|
||||
AquariaLocations.locations_body_c)
|
||||
AquariaLocations.locations_body_c if add_locations else None)
|
||||
self.body_l = self.__add_region("The Body left area",
|
||||
AquariaLocations.locations_body_l)
|
||||
AquariaLocations.locations_body_l if add_locations else None)
|
||||
self.body_rt = self.__add_region("The Body right area, top path",
|
||||
AquariaLocations.locations_body_rt)
|
||||
AquariaLocations.locations_body_rt if add_locations else None)
|
||||
self.body_rb = self.__add_region("The Body right area, bottom path",
|
||||
AquariaLocations.locations_body_rb)
|
||||
AquariaLocations.locations_body_rb if add_locations else None)
|
||||
self.body_b = self.__add_region("The Body bottom area",
|
||||
AquariaLocations.locations_body_b)
|
||||
self.final_boss_loby = self.__add_region("The Body, before final boss", None)
|
||||
AquariaLocations.locations_body_b if add_locations else None)
|
||||
self.final_boss_lobby = self.__add_region("The Body, before final boss", None)
|
||||
self.final_boss_tube = self.__add_region("The Body, final boss area turtle room",
|
||||
AquariaLocations.locations_final_boss_tube)
|
||||
AquariaLocations.locations_final_boss_tube
|
||||
if add_locations else
|
||||
AquariaLocations.locations_final_boss_tube_transturtle_only)
|
||||
self.final_boss = self.__add_region("The Body, final boss",
|
||||
AquariaLocations.locations_final_boss)
|
||||
AquariaLocations.locations_final_boss if add_locations else None)
|
||||
self.final_boss_end = self.__add_region("The Body, final boss area", None)
|
||||
|
||||
def get_entrance_name(self, from_region: Region, to_region: Region):
|
||||
@staticmethod
|
||||
def get_entrance_name(from_region: Region, to_region: Region):
|
||||
|
||||
"""
|
||||
Return the name of an entrance between `from_region` and `to_region`
|
||||
"""
|
||||
@@ -510,6 +551,7 @@ class AquariaRegions:
|
||||
entrance = Entrance(self.player, self.get_entrance_name(source_region, destination_region), source_region)
|
||||
source_region.exits.append(entrance)
|
||||
entrance.connect(destination_region)
|
||||
|
||||
if rule is not None:
|
||||
set_rule(entrance, rule)
|
||||
|
||||
@@ -633,8 +675,8 @@ class AquariaRegions:
|
||||
lambda state: _has_beast_form(state, self.player))
|
||||
self.__connect_regions(self.mithalas_castle, self.cathedral_underground,
|
||||
lambda state: _has_beast_form(state, self.player))
|
||||
self.__connect_one_way_regions(self.mithalas_castle, self.cathedral_top_start,
|
||||
lambda state: _has_bind_song(state, self.player))
|
||||
self.__connect_regions(self.mithalas_castle, self.cathedral_top_start,
|
||||
lambda state: _is_cathedral_door_opened(state, self.player))
|
||||
self.__connect_one_way_regions(self.cathedral_top_start, self.cathedral_top_start_urns,
|
||||
lambda state: _has_damaging_item(state, self.player))
|
||||
self.__connect_regions(self.cathedral_top_start, self.cathedral_top_end,
|
||||
@@ -708,16 +750,14 @@ class AquariaRegions:
|
||||
self.__connect_regions(self.veil_tl, self.turtle_cave)
|
||||
self.__connect_regions(self.turtle_cave, self.turtle_cave_bubble)
|
||||
self.__connect_regions(self.veil_tr_r, self.sun_temple_r)
|
||||
|
||||
self.__connect_one_way_regions(self.sun_temple_r, self.sun_temple_l_entrance,
|
||||
lambda state: _has_bind_song(state, self.player) or
|
||||
lambda state: _has_sun_crystal(state, self.player) or
|
||||
_has_light(state, self.player))
|
||||
self.__connect_one_way_regions(self.sun_temple_l_entrance, self.sun_temple_r,
|
||||
lambda state: _has_light(state, self.player))
|
||||
self.__connect_regions(self.sun_temple_l_entrance, self.veil_tr_l)
|
||||
self.__connect_regions(self.sun_temple_l, self.sun_temple_l_entrance)
|
||||
self.__connect_one_way_regions(self.sun_temple_l, self.sun_temple_boss_path)
|
||||
self.__connect_one_way_regions(self.sun_temple_boss_path, self.sun_temple_l)
|
||||
self.__connect_regions(self.sun_temple_l, self.sun_temple_boss_path)
|
||||
self.__connect_regions(self.sun_temple_boss_path, self.sun_temple_boss,
|
||||
lambda state: _has_energy_attack_item(state, self.player))
|
||||
self.__connect_one_way_regions(self.sun_temple_boss, self.veil_tr_l)
|
||||
@@ -800,17 +840,31 @@ class AquariaRegions:
|
||||
self.__connect_one_way_regions(self.body_rb, self.body_c)
|
||||
self.__connect_regions(self.body_c, self.body_b,
|
||||
lambda state: _has_dual_form(state, self.player))
|
||||
self.__connect_regions(self.body_b, self.final_boss_loby,
|
||||
self.__connect_regions(self.body_b, self.final_boss_lobby,
|
||||
lambda state: _has_dual_form(state, self.player))
|
||||
self.__connect_regions(self.final_boss_loby, self.final_boss_tube,
|
||||
self.__connect_regions(self.final_boss_lobby, self.final_boss_tube,
|
||||
lambda state: _has_nature_form(state, self.player))
|
||||
self.__connect_one_way_regions(self.final_boss_loby, self.final_boss,
|
||||
self.__connect_one_way_regions(self.final_boss_lobby, self.final_boss,
|
||||
lambda state: _has_energy_form(state, self.player) and
|
||||
_has_dual_form(state, self.player) and
|
||||
_has_sun_form(state, self.player) and
|
||||
_has_bind_song(state, self.player))
|
||||
self.__connect_one_way_regions(self.final_boss, self.final_boss_end)
|
||||
|
||||
def __connect_four_gods_end(self, options: AquariaOptions) -> None:
|
||||
"""
|
||||
Connect an entrance for the four gods objective ending
|
||||
"""
|
||||
if options.mini_bosses_to_beat.value > 6:
|
||||
victory_lambda = lambda state: (_has_four_gods_beated(state, self.player) and
|
||||
_has_mini_bosses(state, self.player))
|
||||
elif options.big_bosses_to_beat.value > 0:
|
||||
victory_lambda = lambda state: (_has_four_gods_beated(state, self.player) and
|
||||
_has_mini_bosses_four_gods(state, self.player))
|
||||
else:
|
||||
victory_lambda = lambda state: _has_four_gods_beated(state, self.player)
|
||||
self.__connect_one_way_regions(self.menu, self.four_gods_end, victory_lambda)
|
||||
|
||||
def __connect_transturtle(self, item_target: str, region_source: Region, region_target: Region) -> None:
|
||||
"""Connect a single transturtle to another one"""
|
||||
if region_source != region_target:
|
||||
@@ -824,10 +878,10 @@ class AquariaRegions:
|
||||
self.__connect_transturtle(ItemNames.TRANSTURTLE_OPEN_WATERS, region, self.openwater_tr_turtle)
|
||||
self.__connect_transturtle(ItemNames.TRANSTURTLE_KELP_FOREST, region, self.forest_bl)
|
||||
self.__connect_transturtle(ItemNames.TRANSTURTLE_HOME_WATERS, region, self.home_water_transturtle)
|
||||
self.__connect_transturtle(ItemNames.TRANSTURTLE_ABYSS, region, self.abyss_r_transturtle)
|
||||
self.__connect_transturtle(ItemNames.TRANSTURTLE_BODY, region, self.final_boss_tube)
|
||||
self.__connect_transturtle(ItemNames.TRANSTURTLE_SIMON_SAYS, region, self.simon)
|
||||
self.__connect_transturtle(ItemNames.TRANSTURTLE_ARNASSI_RUINS, region, self.arnassi_cave_transturtle)
|
||||
self.__connect_transturtle(ItemNames.TRANSTURTLE_ABYSS, region, self.abyss_r_transturtle)
|
||||
self.__connect_transturtle(ItemNames.TRANSTURTLE_BODY, region, self.final_boss_tube)
|
||||
|
||||
def __connect_transturtles(self) -> None:
|
||||
"""Connect every transturtle with others"""
|
||||
@@ -836,12 +890,12 @@ class AquariaRegions:
|
||||
self._connect_transturtle_to_other(self.openwater_tr_turtle)
|
||||
self._connect_transturtle_to_other(self.forest_bl)
|
||||
self._connect_transturtle_to_other(self.home_water_transturtle)
|
||||
self._connect_transturtle_to_other(self.abyss_r_transturtle)
|
||||
self._connect_transturtle_to_other(self.final_boss_tube)
|
||||
self._connect_transturtle_to_other(self.simon)
|
||||
self._connect_transturtle_to_other(self.arnassi_cave_transturtle)
|
||||
self._connect_transturtle_to_other(self.abyss_r_transturtle)
|
||||
self._connect_transturtle_to_other(self.final_boss_tube)
|
||||
|
||||
def connect_regions(self) -> None:
|
||||
def connect_regions(self, options: AquariaOptions) -> None:
|
||||
"""
|
||||
Connect every region (entrances and exits)
|
||||
"""
|
||||
@@ -853,6 +907,8 @@ class AquariaRegions:
|
||||
self.__connect_abyss_regions()
|
||||
self.__connect_sunken_city_regions()
|
||||
self.__connect_body_regions()
|
||||
if self.is_four_gods:
|
||||
self.__connect_four_gods_end(options)
|
||||
self.__connect_transturtles()
|
||||
|
||||
def __add_event_location(self, region: Region, name: str, event_name: str) -> None:
|
||||
@@ -934,21 +990,25 @@ class AquariaRegions:
|
||||
AquariaLocationNames.THIRD_SECRET,
|
||||
ItemNames.THIRD_SECRET_OBTAINED)
|
||||
|
||||
def add_event_locations(self) -> None:
|
||||
def add_event_locations(self, options: AquariaOptions) -> None:
|
||||
"""
|
||||
Add every event (locations and items) to the `world`
|
||||
"""
|
||||
self.__add_event_mini_bosses()
|
||||
self.__add_event_big_bosses()
|
||||
self.__add_event_secrets()
|
||||
self.__add_event_location(self.sunken_city_boss,
|
||||
AquariaLocationNames.SUNKEN_CITY_CLEARED,
|
||||
ItemNames.BODY_TONGUE_CLEARED)
|
||||
self.__add_event_location(self.sun_temple_r,
|
||||
AquariaLocationNames.SUN_CRYSTAL,
|
||||
ItemNames.HAS_SUN_CRYSTAL)
|
||||
self.__add_event_location(self.final_boss_end, AquariaLocationNames.OBJECTIVE_COMPLETE,
|
||||
ItemNames.VICTORY)
|
||||
self.__add_event_big_bosses()
|
||||
self.__add_event_location(self.sunken_city_boss,
|
||||
AquariaLocationNames.SUNKEN_CITY_CLEARED,
|
||||
ItemNames.BODY_TONGUE_CLEARED)
|
||||
if self.is_four_gods:
|
||||
self.__add_event_location(self.four_gods_end, AquariaLocationNames.OBJECTIVE_COMPLETE,
|
||||
ItemNames.VICTORY)
|
||||
else:
|
||||
self.__add_event_secrets()
|
||||
self.__add_event_location(self.final_boss_end, AquariaLocationNames.OBJECTIVE_COMPLETE,
|
||||
ItemNames.VICTORY)
|
||||
|
||||
def __adjusting_soup_rules(self) -> None:
|
||||
"""
|
||||
@@ -958,16 +1018,16 @@ class AquariaRegions:
|
||||
lambda state: _has_hot_soup(state, self.player))
|
||||
add_rule(self.multiworld.get_location(AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_FIRST_CLIFF_BULB, self.player),
|
||||
lambda state: _has_beast_and_soup_form(state, self.player) or
|
||||
state.has(ItemNames.LUMEREAN_GOD_BEATED, self.player), combine="or")
|
||||
state.has(ItemNames.LUMEREAN_GOD_BEATED, self.player))
|
||||
add_rule(self.multiworld.get_location(AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_SECOND_CLIFF_BULB, self.player),
|
||||
lambda state: _has_beast_and_soup_form(state, self.player) or
|
||||
state.has(ItemNames.LUMEREAN_GOD_BEATED, self.player), combine="or")
|
||||
state.has(ItemNames.LUMEREAN_GOD_BEATED, self.player))
|
||||
add_rule(
|
||||
self.multiworld.get_location(AquariaLocationNames.THE_VEIL_TOP_RIGHT_AREA_BULB_AT_THE_TOP_OF_THE_WATERFALL,
|
||||
self.player),
|
||||
lambda state: _has_beast_and_soup_form(state, self.player))
|
||||
|
||||
def __adjusting_under_rock_location(self) -> None:
|
||||
def __adjusting_under_rock_location(self, options: AquariaOptions) -> None:
|
||||
"""
|
||||
Modify rules implying bind song needed for bulb under rocks
|
||||
"""
|
||||
@@ -999,21 +1059,29 @@ class AquariaRegions:
|
||||
add_rule(self.multiworld.get_location(
|
||||
AquariaLocationNames.THE_VEIL_TOP_LEFT_AREA_BULB_UNDER_THE_ROCK_IN_THE_TOP_RIGHT_PATH,
|
||||
self.player), lambda state: _has_bind_song(state, self.player))
|
||||
add_rule(
|
||||
self.multiworld.get_location(AquariaLocationNames.ABYSS_RIGHT_AREA_BULB_BEHIND_THE_ROCK_IN_THE_WHALE_ROOM,
|
||||
self.player), lambda state: _has_bind_song(state, self.player))
|
||||
add_rule(self.multiworld.get_location(AquariaLocationNames.ABYSS_RIGHT_AREA_BULB_IN_THE_MIDDLE_PATH,
|
||||
self.player), lambda state: _has_bind_song(state, self.player))
|
||||
add_rule(self.multiworld.get_location(
|
||||
AquariaLocationNames.THE_VEIL_TOP_LEFT_AREA_BULB_UNDER_THE_ROCK_IN_THE_TOP_RIGHT_PATH,
|
||||
self.player), lambda state: _has_bind_song(state, self.player))
|
||||
if not self.is_four_gods:
|
||||
add_rule(self.multiworld.get_location(
|
||||
AquariaLocationNames.ABYSS_RIGHT_AREA_BULB_BEHIND_THE_ROCK_IN_THE_WHALE_ROOM,
|
||||
self.player), lambda state: _has_bind_song(state, self.player))
|
||||
add_rule(self.multiworld.get_location(AquariaLocationNames.ABYSS_RIGHT_AREA_BULB_IN_THE_MIDDLE_PATH,
|
||||
self.player), lambda state: _has_bind_song(state, self.player))
|
||||
|
||||
def __adjusting_light_in_dark_place_rules(self) -> None:
|
||||
def __adjusting_light_in_dark_place_rules(self, light_option: LightNeededToGetToDarkPlaces) -> None:
|
||||
"""
|
||||
Modify rules implying that the player needs a light to go in dark places
|
||||
"""
|
||||
if light_option == LightNeededToGetToDarkPlaces.option_sun_form:
|
||||
light_lambda = _has_sun_form
|
||||
else:
|
||||
light_lambda = _has_light
|
||||
add_rule(self.multiworld.get_location(AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BLACK_PEARL, self.player),
|
||||
lambda state: _has_light(state, self.player))
|
||||
lambda state: light_lambda(state, self.player))
|
||||
add_rule(
|
||||
self.multiworld.get_location(AquariaLocationNames.KELP_FOREST_BOTTOM_RIGHT_AREA_ODD_CONTAINER, self.player),
|
||||
lambda state: _has_light(state, self.player))
|
||||
lambda state: light_lambda(state, self.player))
|
||||
add_rule(self.multiworld.get_entrance(self.get_entrance_name(self.sun_temple_l_entrance, self.sun_temple_l),
|
||||
self.player), lambda state: _has_light(state, self.player) or
|
||||
_has_sun_crystal(state, self.player))
|
||||
@@ -1022,15 +1090,15 @@ class AquariaRegions:
|
||||
_has_sun_crystal(state, self.player))
|
||||
add_rule(self.multiworld.get_entrance(self.get_entrance_name(self.abyss_r_transturtle, self.abyss_r),
|
||||
self.player),
|
||||
lambda state: _has_light(state, self.player))
|
||||
lambda state: light_lambda(state, self.player))
|
||||
add_rule(self.multiworld.get_entrance(self.get_entrance_name(self.body_c, self.abyss_lb), self.player),
|
||||
lambda state: _has_light(state, self.player))
|
||||
lambda state: light_lambda(state, self.player))
|
||||
add_rule(self.multiworld.get_entrance(self.get_entrance_name(self.openwater_br, self.abyss_r), self.player),
|
||||
lambda state: _has_light(state, self.player))
|
||||
lambda state: light_lambda(state, self.player))
|
||||
add_rule(self.multiworld.get_entrance(self.get_entrance_name(self.openwater_bl, self.abyss_l), self.player),
|
||||
lambda state: _has_light(state, self.player))
|
||||
lambda state: light_lambda(state, self.player))
|
||||
|
||||
def __adjusting_manual_rules(self) -> None:
|
||||
def __adjusting_manual_rules(self, options: AquariaOptions) -> None:
|
||||
add_rule(self.multiworld.get_location(AquariaLocationNames.MITHALAS_CATHEDRAL_MITHALAN_DRESS, self.player),
|
||||
lambda state: _has_beast_form(state, self.player))
|
||||
add_rule(self.multiworld.get_location(
|
||||
@@ -1045,9 +1113,6 @@ class AquariaRegions:
|
||||
lambda state: _has_bind_song(state, self.player))
|
||||
add_rule(self.multiworld.get_location(AquariaLocationNames.TURTLE_CAVE_TURTLE_EGG, self.player),
|
||||
lambda state: _has_bind_song(state, self.player))
|
||||
add_rule(self.multiworld.get_location(AquariaLocationNames.ABYSS_LEFT_AREA_BULB_IN_THE_BOTTOM_FISH_PASS,
|
||||
self.player),
|
||||
lambda state: _has_fish_form(state, self.player))
|
||||
add_rule(self.multiworld.get_location(AquariaLocationNames.SONG_CAVE_ANEMONE_SEED, self.player),
|
||||
lambda state: _has_nature_form(state, self.player))
|
||||
add_rule(self.multiworld.get_location(AquariaLocationNames.SONG_CAVE_VERSE_EGG, self.player),
|
||||
@@ -1076,19 +1141,21 @@ class AquariaRegions:
|
||||
), lambda state: _has_beast_form_or_arnassi_armor(state, self.player))
|
||||
add_rule(self.multiworld.get_location(AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_JELLY_EGG, self.player),
|
||||
lambda state: _has_beast_form(state, self.player))
|
||||
add_rule(self.multiworld.get_location(AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_FIRST_CLIFF_BULB, self.player),
|
||||
lambda state: state.has("Sun God beated", self.player))
|
||||
add_rule(self.multiworld.get_location(AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_SECOND_CLIFF_BULB, self.player),
|
||||
lambda state: state.has("Sun God beated", self.player))
|
||||
add_rule(
|
||||
self.multiworld.get_location(AquariaLocationNames.THE_BODY_CENTER_AREA_BREAKING_LI_S_CAGE, self.player),
|
||||
lambda state: _has_tongue_cleared(state, self.player))
|
||||
add_rule(self.multiworld.get_location(
|
||||
AquariaLocationNames.OPEN_WATERS_TOP_RIGHT_AREA_BULB_IN_THE_SMALL_PATH_BEFORE_MITHALAS,
|
||||
self.player), lambda state: _has_bind_song(state, self.player)
|
||||
)
|
||||
add_rule(self.multiworld.get_location(AquariaLocationNames.SITTING_ON_THRONE, self.player),
|
||||
lambda state: _has_bind_song(state, self.player))
|
||||
if not self.is_four_gods:
|
||||
add_rule(self.multiworld.get_location(AquariaLocationNames.ABYSS_LEFT_AREA_BULB_IN_THE_BOTTOM_FISH_PASS,
|
||||
self.player),
|
||||
lambda state: _has_fish_form(state, self.player))
|
||||
add_rule(
|
||||
self.multiworld.get_location(AquariaLocationNames.THE_BODY_CENTER_AREA_BREAKING_LI_S_CAGE, self.player),
|
||||
lambda state: _has_tongue_cleared(state, self.player))
|
||||
|
||||
def __no_progression_hard_or_hidden_location(self) -> None:
|
||||
def __no_progression_hard_or_hidden_location(self, options: AquariaOptions) -> None:
|
||||
self.multiworld.get_location(AquariaLocationNames.ENERGY_TEMPLE_BOSS_AREA_FALLEN_GOD_TOOTH,
|
||||
self.player).item_rule = _item_not_advancement
|
||||
self.multiworld.get_location(AquariaLocationNames.MITHALAS_BOSS_AREA_BEATING_MITHALAN_GOD,
|
||||
@@ -1097,8 +1164,6 @@ class AquariaRegions:
|
||||
self.player).item_rule = _item_not_advancement
|
||||
self.multiworld.get_location(AquariaLocationNames.SUN_TEMPLE_BOSS_AREA_BEATING_LUMEREAN_GOD,
|
||||
self.player).item_rule = _item_not_advancement
|
||||
self.multiworld.get_location(AquariaLocationNames.SUNKEN_CITY_BULB_ON_TOP_OF_THE_BOSS_AREA,
|
||||
self.player).item_rule = _item_not_advancement
|
||||
self.multiworld.get_location(AquariaLocationNames.HOME_WATERS_NAUTILUS_EGG,
|
||||
self.player).item_rule = _item_not_advancement
|
||||
self.multiworld.get_location(AquariaLocationNames.ENERGY_TEMPLE_BLASTER_ROOM_BLASTER_EGG,
|
||||
@@ -1109,25 +1174,12 @@ class AquariaRegions:
|
||||
self.player).item_rule = _item_not_advancement
|
||||
self.multiworld.get_location(AquariaLocationNames.OCTOPUS_CAVE_DUMBO_EGG,
|
||||
self.player).item_rule = _item_not_advancement
|
||||
self.multiworld.get_location(AquariaLocationNames.KING_JELLYFISH_CAVE_BULB_IN_THE_RIGHT_PATH_FROM_KING_JELLY,
|
||||
self.player).item_rule = _item_not_advancement
|
||||
self.multiworld.get_location(AquariaLocationNames.KING_JELLYFISH_CAVE_JELLYFISH_COSTUME,
|
||||
self.player).item_rule = _item_not_advancement
|
||||
self.multiworld.get_location(AquariaLocationNames.FINAL_BOSS_AREA_BULB_IN_THE_BOSS_THIRD_FORM_ROOM,
|
||||
self.player).item_rule = _item_not_advancement
|
||||
self.multiworld.get_location(AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_FIRST_CLIFF_BULB,
|
||||
self.player).item_rule = _item_not_advancement
|
||||
self.multiworld.get_location(AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_SECOND_CLIFF_BULB,
|
||||
self.player).item_rule = _item_not_advancement
|
||||
self.multiworld.get_location(AquariaLocationNames.THE_VEIL_TOP_RIGHT_AREA_BULB_AT_THE_TOP_OF_THE_WATERFALL,
|
||||
self.player).item_rule = _item_not_advancement
|
||||
self.multiworld.get_location(AquariaLocationNames.BUBBLE_CAVE_BULB_IN_THE_LEFT_CAVE_WALL,
|
||||
self.player).item_rule = _item_not_advancement
|
||||
self.multiworld.get_location(
|
||||
AquariaLocationNames.BUBBLE_CAVE_BULB_IN_THE_RIGHT_CAVE_WALL_BEHIND_THE_ICE_CRYSTAL,
|
||||
self.player).item_rule = _item_not_advancement
|
||||
self.multiworld.get_location(AquariaLocationNames.BUBBLE_CAVE_VERSE_EGG,
|
||||
self.player).item_rule = _item_not_advancement
|
||||
self.multiworld.get_location(
|
||||
AquariaLocationNames.KELP_FOREST_BOTTOM_LEFT_AREA_BULB_CLOSE_TO_THE_SPIRIT_CRYSTALS,
|
||||
self.player).item_rule = _item_not_advancement
|
||||
@@ -1135,33 +1187,183 @@ class AquariaRegions:
|
||||
self.player).item_rule = _item_not_advancement
|
||||
self.multiworld.get_location(AquariaLocationNames.SUN_TEMPLE_SUN_KEY,
|
||||
self.player).item_rule = _item_not_advancement
|
||||
self.multiworld.get_location(AquariaLocationNames.THE_BODY_BOTTOM_AREA_MUTANT_COSTUME,
|
||||
self.player).item_rule = _item_not_advancement
|
||||
self.multiworld.get_location(AquariaLocationNames.SUN_TEMPLE_BULB_IN_THE_HIDDEN_ROOM_OF_THE_RIGHT_PART,
|
||||
self.player).item_rule = _item_not_advancement
|
||||
self.multiworld.get_location(AquariaLocationNames.ARNASSI_RUINS_ARNASSI_ARMOR,
|
||||
self.player).item_rule = _item_not_advancement
|
||||
if not self.is_four_gods:
|
||||
self.multiworld.get_location(AquariaLocationNames.SUNKEN_CITY_BULB_ON_TOP_OF_THE_BOSS_AREA,
|
||||
self.player).item_rule = _item_not_advancement
|
||||
self.multiworld.get_location(AquariaLocationNames.THE_BODY_BOTTOM_AREA_MUTANT_COSTUME,
|
||||
self.player).item_rule = _item_not_advancement
|
||||
self.multiworld.get_location(AquariaLocationNames.FINAL_BOSS_AREA_BULB_IN_THE_BOSS_THIRD_FORM_ROOM,
|
||||
self.player).item_rule = _item_not_advancement
|
||||
self.multiworld.get_location(
|
||||
AquariaLocationNames.KING_JELLYFISH_CAVE_BULB_IN_THE_RIGHT_PATH_FROM_KING_JELLY,
|
||||
self.player).item_rule = _item_not_advancement
|
||||
self.multiworld.get_location(AquariaLocationNames.KING_JELLYFISH_CAVE_JELLYFISH_COSTUME,
|
||||
self.player).item_rule = _item_not_advancement
|
||||
self.multiworld.get_location(AquariaLocationNames.BUBBLE_CAVE_BULB_IN_THE_LEFT_CAVE_WALL,
|
||||
self.player).item_rule = _item_not_advancement
|
||||
self.multiworld.get_location(
|
||||
AquariaLocationNames.BUBBLE_CAVE_BULB_IN_THE_RIGHT_CAVE_WALL_BEHIND_THE_ICE_CRYSTAL,
|
||||
self.player).item_rule = _item_not_advancement
|
||||
self.multiworld.get_location(AquariaLocationNames.BUBBLE_CAVE_VERSE_EGG,
|
||||
self.player).item_rule = _item_not_advancement
|
||||
|
||||
def __no_progression_area(self, area: dict) -> None:
|
||||
"""Be sure to not put any progression items in location of an `area`"""
|
||||
for location in area:
|
||||
self.multiworld.get_location(location, self.player).item_rule = _item_not_advancement
|
||||
|
||||
def __no_progression_kelp_forest(self) -> None:
|
||||
"""Be sure to not put any progression items in Kelp forest"""
|
||||
self.__no_progression_area(AquariaLocations.locations_forest_tl)
|
||||
self.__no_progression_area(AquariaLocations.locations_forest_tl_verse_egg_room)
|
||||
self.__no_progression_area(AquariaLocations.locations_forest_tr)
|
||||
self.__no_progression_area(AquariaLocations.locations_forest_tr_fp)
|
||||
self.__no_progression_area(AquariaLocations.locations_forest_bl)
|
||||
self.__no_progression_area(AquariaLocations.locations_forest_bl_sc)
|
||||
self.__no_progression_area(AquariaLocations.locations_forest_br)
|
||||
self.__no_progression_area(AquariaLocations.locations_forest_boss)
|
||||
self.__no_progression_area(AquariaLocations.locations_forest_boss_entrance)
|
||||
self.__no_progression_area(AquariaLocations.locations_forest_fish_cave)
|
||||
self.__no_progression_area(AquariaLocations.locations_sprite_cave)
|
||||
self.__no_progression_area(AquariaLocations.locations_sprite_cave_tube)
|
||||
self.__no_progression_area(AquariaLocations.locations_mermog_cave)
|
||||
self.__no_progression_area(AquariaLocations.locations_mermog_boss)
|
||||
|
||||
def __no_progression_veil(self) -> None:
|
||||
"""Be sure to not put any progression items in The Veil"""
|
||||
self.__no_progression_area(AquariaLocations.locations_veil_tl)
|
||||
self.__no_progression_area(AquariaLocations.locations_veil_tl_fp)
|
||||
self.__no_progression_area(AquariaLocations.locations_turtle_cave)
|
||||
self.__no_progression_area(AquariaLocations.locations_turtle_cave_bubble)
|
||||
self.__no_progression_area(AquariaLocations.locations_veil_tr_r)
|
||||
self.__no_progression_area(AquariaLocations.locations_veil_tr_l)
|
||||
self.__no_progression_area(AquariaLocations.locations_veil_b)
|
||||
self.__no_progression_area(AquariaLocations.locations_veil_b_sc)
|
||||
self.__no_progression_area(AquariaLocations.locations_veil_b_fp)
|
||||
self.__no_progression_area(AquariaLocations.locations_veil_br)
|
||||
self.__no_progression_area(AquariaLocations.locations_octo_cave_t)
|
||||
self.__no_progression_area(AquariaLocations.locations_octo_cave_b)
|
||||
self.__no_progression_area(AquariaLocations.locations_sun_temple_l)
|
||||
self.__no_progression_area(AquariaLocations.locations_sun_temple_r)
|
||||
self.__no_progression_area(AquariaLocations.locations_sun_temple_boss_path)
|
||||
self.__no_progression_area(AquariaLocations.locations_sun_temple_boss)
|
||||
|
||||
def __no_progression_mithalas(self) -> None:
|
||||
"""Be sure to not put any progression items in Mithalas"""
|
||||
self.__no_progression_area(AquariaLocations.locations_mithalas_city)
|
||||
self.__no_progression_area(AquariaLocations.locations_mithalas_city_urns)
|
||||
self.__no_progression_area(AquariaLocations.locations_mithalas_city_top_path)
|
||||
self.__no_progression_area(AquariaLocations.locations_mithalas_city_fishpass)
|
||||
self.__no_progression_area(AquariaLocations.locations_mithalas_castle)
|
||||
self.__no_progression_area(AquariaLocations.locations_mithalas_castle_urns)
|
||||
self.__no_progression_area(AquariaLocations.locations_mithalas_castle_tube)
|
||||
self.__no_progression_area(AquariaLocations.locations_mithalas_castle_sc)
|
||||
self.__no_progression_area(AquariaLocations.locations_cathedral_top_start)
|
||||
self.__no_progression_area(AquariaLocations.locations_cathedral_top_start_urns)
|
||||
self.__no_progression_area(AquariaLocations.locations_cathedral_top_end)
|
||||
self.__no_progression_area(AquariaLocations.locations_cathedral_underground)
|
||||
self.__no_progression_area(AquariaLocations.locations_cathedral_boss)
|
||||
|
||||
def __no_progression_energy_temple(self) -> None:
|
||||
"""Be sure to not put any progression items in the Energy Temple"""
|
||||
self.__no_progression_area(AquariaLocations.locations_energy_temple_1)
|
||||
self.__no_progression_area(AquariaLocations.locations_energy_temple_idol)
|
||||
self.__no_progression_area(AquariaLocations.locations_energy_temple_2)
|
||||
self.__no_progression_area(AquariaLocations.locations_energy_temple_altar)
|
||||
self.__no_progression_area(AquariaLocations.locations_energy_temple_3)
|
||||
self.__no_progression_area(AquariaLocations.locations_energy_temple_boss)
|
||||
self.__no_progression_area(AquariaLocations.locations_energy_temple_blaster_room)
|
||||
|
||||
def __no_progression_arnassi_ruins(self, options: AquariaOptions) -> None:
|
||||
"""Be sure to not put any progression items in the Arnassi ruins"""
|
||||
self.__no_progression_area(AquariaLocations.locations_arnassi)
|
||||
self.__no_progression_area(AquariaLocations.locations_arnassi_cave)
|
||||
self.__no_progression_area(AquariaLocations.locations_arnassi_cave_transturtle)
|
||||
self.__no_progression_area(AquariaLocations.locations_arnassi_crab_boss)
|
||||
if options.turtle_randomizer == 0:
|
||||
self.__no_progression_area(AquariaLocations.locations_simon)
|
||||
|
||||
def __no_progression_frozen_veil(self) -> None:
|
||||
"""Be sure to not put any progression items in the Frozen Veil"""
|
||||
self.__no_progression_area(AquariaLocations.locations_ice_cave)
|
||||
self.__no_progression_area(AquariaLocations.locations_bubble_cave)
|
||||
self.__no_progression_area(AquariaLocations.locations_bubble_cave_boss)
|
||||
|
||||
def __no_progression_abyss(self) -> None:
|
||||
"""Be sure to not put any progression items in the Abyss"""
|
||||
self.__no_progression_area(AquariaLocations.locations_abyss_l)
|
||||
self.__no_progression_area(AquariaLocations.locations_abyss_lb)
|
||||
self.__no_progression_area(AquariaLocations.locations_abyss_r)
|
||||
self.__no_progression_area(AquariaLocations.locations_abyss_r_whale)
|
||||
self.__no_progression_area(AquariaLocations.locations_abyss_r_transturtle)
|
||||
self.__no_progression_area(AquariaLocations.locations_king_jellyfish_cave)
|
||||
|
||||
def __no_progression_sunken_city(self) -> None:
|
||||
"""Be sure to not put any progression items in the Sunken City"""
|
||||
self.__no_progression_area(AquariaLocations.locations_sunken_city_r)
|
||||
self.__no_progression_area(AquariaLocations.locations_sunken_city_l)
|
||||
self.__no_progression_area(AquariaLocations.locations_sunken_city_l_bedroom)
|
||||
self.__no_progression_area(AquariaLocations.locations_sunken_city_boss)
|
||||
|
||||
def __no_progression_body(self) -> None:
|
||||
"""Be sure to not put any progression items in the Body"""
|
||||
self.__no_progression_area(AquariaLocations.locations_body_c)
|
||||
self.__no_progression_area(AquariaLocations.locations_body_l)
|
||||
self.__no_progression_area(AquariaLocations.locations_body_rt)
|
||||
self.__no_progression_area(AquariaLocations.locations_body_rb)
|
||||
self.__no_progression_area(AquariaLocations.locations_body_b)
|
||||
self.__no_progression_area(AquariaLocations.locations_final_boss_tube)
|
||||
self.__no_progression_area(AquariaLocations.locations_final_boss)
|
||||
|
||||
def __no_progression_areas(self, options: AquariaOptions) -> None:
|
||||
"""Manage options that remove progression items from areas around the Aquaria world"""
|
||||
if options.no_progression_simon_says:
|
||||
self.__no_progression_area(AquariaLocations.locations_simon)
|
||||
if options.no_progression_kelp_forest:
|
||||
self.__no_progression_kelp_forest()
|
||||
if options.no_progression_veil:
|
||||
self.__no_progression_veil()
|
||||
if options.no_progression_mithalas:
|
||||
self.__no_progression_mithalas()
|
||||
if options.no_progression_energy_temple:
|
||||
self.__no_progression_energy_temple()
|
||||
if options.no_progression_arnassi_ruins:
|
||||
self.__no_progression_arnassi_ruins(options)
|
||||
if not self.is_four_gods:
|
||||
if options.no_progression_frozen_veil:
|
||||
self.__no_progression_frozen_veil()
|
||||
if options.no_progression_abyss:
|
||||
self.__no_progression_abyss()
|
||||
if options.no_progression_sunken_city:
|
||||
self.__no_progression_sunken_city()
|
||||
if options.no_progression_body:
|
||||
self.__no_progression_body()
|
||||
|
||||
def adjusting_rules(self, options: AquariaOptions) -> None:
|
||||
"""
|
||||
Modify rules for single location or optional rules
|
||||
"""
|
||||
self.__adjusting_manual_rules()
|
||||
self.__adjusting_manual_rules(options)
|
||||
self.__adjusting_soup_rules()
|
||||
if options.light_needed_to_get_to_dark_places:
|
||||
self.__adjusting_light_in_dark_place_rules()
|
||||
self.__no_progression_areas(options)
|
||||
if options.light_needed_to_get_to_dark_places != LightNeededToGetToDarkPlaces.option_off:
|
||||
self.__adjusting_light_in_dark_place_rules(options.light_needed_to_get_to_dark_places)
|
||||
if options.bind_song_needed_to_get_under_rock_bulb:
|
||||
self.__adjusting_under_rock_location()
|
||||
|
||||
if options.mini_bosses_to_beat.value > 0:
|
||||
add_rule(self.multiworld.get_entrance(self.get_entrance_name(self.final_boss_loby, self.final_boss),
|
||||
self.player), lambda state: _has_mini_bosses(state, self.player))
|
||||
if options.big_bosses_to_beat.value > 0:
|
||||
add_rule(self.multiworld.get_entrance(self.get_entrance_name(self.final_boss_loby, self.final_boss),
|
||||
self.player), lambda state: _has_big_bosses(state, self.player))
|
||||
if options.objective.value == options.objective.option_obtain_secrets_and_kill_the_creator:
|
||||
add_rule(self.multiworld.get_entrance(self.get_entrance_name(self.final_boss_loby, self.final_boss),
|
||||
self.player), lambda state: _has_secrets(state, self.player))
|
||||
self.__adjusting_under_rock_location(options)
|
||||
if not self.is_four_gods:
|
||||
if options.mini_bosses_to_beat.value > 0:
|
||||
add_rule(self.multiworld.get_entrance(self.get_entrance_name(self.final_boss_lobby, self.final_boss),
|
||||
self.player), lambda state: _has_mini_bosses(state, self.player))
|
||||
if options.big_bosses_to_beat.value > 0:
|
||||
add_rule(self.multiworld.get_entrance(self.get_entrance_name(self.final_boss_lobby, self.final_boss),
|
||||
self.player), lambda state: _has_big_bosses(state, self.player))
|
||||
if options.objective.value == options.objective.option_obtain_secrets_and_kill_the_creator:
|
||||
add_rule(self.multiworld.get_entrance(self.get_entrance_name(self.final_boss_lobby, self.final_boss),
|
||||
self.player), lambda state: _has_secrets(state, self.player))
|
||||
if (options.unconfine_home_water.value == UnconfineHomeWater.option_via_energy_door or
|
||||
options.unconfine_home_water.value == UnconfineHomeWater.option_off):
|
||||
add_rule(self.multiworld.get_entrance(self.get_entrance_name(self.home_water, self.home_water_transturtle),
|
||||
@@ -1173,7 +1375,7 @@ class AquariaRegions:
|
||||
lambda state: _has_bind_song(state, self.player) and
|
||||
_has_energy_attack_item(state, self.player))
|
||||
if options.no_progression_hard_or_hidden_locations:
|
||||
self.__no_progression_hard_or_hidden_location()
|
||||
self.__no_progression_hard_or_hidden_location(options)
|
||||
|
||||
def __add_home_water_regions_to_world(self) -> None:
|
||||
"""
|
||||
@@ -1298,12 +1500,12 @@ class AquariaRegions:
|
||||
self.multiworld.regions.append(self.body_rt)
|
||||
self.multiworld.regions.append(self.body_rb)
|
||||
self.multiworld.regions.append(self.body_b)
|
||||
self.multiworld.regions.append(self.final_boss_loby)
|
||||
self.multiworld.regions.append(self.final_boss_lobby)
|
||||
self.multiworld.regions.append(self.final_boss_tube)
|
||||
self.multiworld.regions.append(self.final_boss)
|
||||
self.multiworld.regions.append(self.final_boss_end)
|
||||
|
||||
def add_regions_to_world(self) -> None:
|
||||
def add_regions_to_world(self, options: AquariaOptions) -> None:
|
||||
"""
|
||||
Add every region to the `world`
|
||||
"""
|
||||
@@ -1314,13 +1516,17 @@ class AquariaRegions:
|
||||
self.__add_veil_regions_to_world()
|
||||
self.__add_abyss_regions_to_world()
|
||||
self.__add_body_regions_to_world()
|
||||
if self.is_four_gods:
|
||||
self.multiworld.regions.append(self.four_gods_end)
|
||||
|
||||
def __init__(self, multiworld: MultiWorld, player: int):
|
||||
def __init__(self, multiworld: MultiWorld, player: int, options: AquariaOptions):
|
||||
"""
|
||||
Initialisation of the regions
|
||||
"""
|
||||
self.multiworld = multiworld
|
||||
self.player = player
|
||||
self.is_four_gods = (options.objective.value == Objective.option_killing_the_four_gods or
|
||||
options.objective.value == Objective.option_gods_and_creator)
|
||||
self.__create_home_water_area()
|
||||
self.__create_energy_temple()
|
||||
self.__create_openwater()
|
||||
@@ -1328,6 +1534,8 @@ class AquariaRegions:
|
||||
self.__create_forest()
|
||||
self.__create_veil()
|
||||
self.__create_sun_temple()
|
||||
self.__create_abyss()
|
||||
self.__create_sunken_city()
|
||||
self.__create_body()
|
||||
self.__create_abyss(not self.is_four_gods)
|
||||
self.__create_sunken_city(not self.is_four_gods)
|
||||
self.__create_body(not self.is_four_gods)
|
||||
if self.is_four_gods:
|
||||
self.four_gods_end = self.__add_region("Four gods ending", None)
|
||||
|
||||
@@ -7,12 +7,14 @@ Description: Main module for Aquaria game multiworld randomizer
|
||||
from typing import List, Dict, ClassVar, Any
|
||||
from worlds.AutoWorld import World, WebWorld
|
||||
from BaseClasses import Tutorial, MultiWorld, ItemClassification
|
||||
from .Items import item_table, AquariaItem, ItemType, ItemGroup, ItemNames
|
||||
from .Items import item_table, AquariaItem, ItemType, ItemGroup, ItemNames, four_gods_excludes
|
||||
from .Locations import location_table, AquariaLocationNames
|
||||
from .Options import (AquariaOptions, IngredientRandomizer, TurtleRandomizer, EarlyBindSong, EarlyEnergyForm,
|
||||
UnconfineHomeWater, Objective)
|
||||
from .Regions import AquariaRegions
|
||||
|
||||
CLIENT_MINIMAL_COMPATIBILITY = [1, 4, 1]
|
||||
|
||||
|
||||
class AquariaWeb(WebWorld):
|
||||
"""
|
||||
@@ -98,6 +100,7 @@ class AquariaWorld(World):
|
||||
"Used to manage Regions"
|
||||
|
||||
exclude: List[str]
|
||||
"The items that should not be added to the multiworld item pool"
|
||||
|
||||
def __init__(self, multiworld: MultiWorld, player: int):
|
||||
"""Initialisation of the Aquaria World"""
|
||||
@@ -111,15 +114,15 @@ class AquariaWorld(World):
|
||||
Run before any general steps of the MultiWorld other than options. Useful for getting and adjusting option
|
||||
results and determining layouts for entrance rando etc. start inventory gets pushed after this step.
|
||||
"""
|
||||
self.regions = AquariaRegions(self.multiworld, self.player)
|
||||
self.regions = AquariaRegions(self.multiworld, self.player, self.options)
|
||||
|
||||
def create_regions(self) -> None:
|
||||
"""
|
||||
Create every Region in `regions`
|
||||
"""
|
||||
self.regions.add_regions_to_world()
|
||||
self.regions.connect_regions()
|
||||
self.regions.add_event_locations()
|
||||
self.regions.add_regions_to_world(self.options)
|
||||
self.regions.connect_regions(self.options)
|
||||
self.regions.add_event_locations(self.options)
|
||||
|
||||
def create_item(self, name: str) -> AquariaItem:
|
||||
"""
|
||||
@@ -157,8 +160,17 @@ class AquariaWorld(World):
|
||||
def create_items(self) -> None:
|
||||
"""Create every item in the world"""
|
||||
precollected = [item.name for item in self.multiworld.precollected_items[self.player]]
|
||||
if (self.options.objective.value == Objective.option_killing_the_four_gods or
|
||||
self.options.objective.value == Objective.option_gods_and_creator):
|
||||
self.exclude.extend(four_gods_excludes)
|
||||
self.__pre_fill_item(ItemNames.TRANSTURTLE_ABYSS, AquariaLocationNames.ABYSS_RIGHT_AREA_TRANSTURTLE,
|
||||
precollected)
|
||||
self.__pre_fill_item(ItemNames.TRANSTURTLE_BODY, AquariaLocationNames.FINAL_BOSS_AREA_TRANSTURTLE,
|
||||
precollected)
|
||||
if self.options.turtle_randomizer.value != TurtleRandomizer.option_none:
|
||||
if self.options.turtle_randomizer.value == TurtleRandomizer.option_all_except_final:
|
||||
if (self.options.turtle_randomizer.value == TurtleRandomizer.option_all_except_final and
|
||||
self.options.objective.value != Objective.option_killing_the_four_gods and
|
||||
self.options.objective.value != Objective.option_gods_and_creator):
|
||||
self.__pre_fill_item(ItemNames.TRANSTURTLE_BODY, AquariaLocationNames.FINAL_BOSS_AREA_TRANSTURTLE,
|
||||
precollected)
|
||||
else:
|
||||
@@ -167,25 +179,29 @@ class AquariaWorld(World):
|
||||
self.__pre_fill_item(ItemNames.TRANSTURTLE_VEIL_TOP_RIGHT,
|
||||
AquariaLocationNames.THE_VEIL_TOP_RIGHT_AREA_TRANSTURTLE, precollected)
|
||||
self.__pre_fill_item(ItemNames.TRANSTURTLE_OPEN_WATERS,
|
||||
AquariaLocationNames.OPEN_WATERS_TOP_RIGHT_AREA_TRANSTURTLE,
|
||||
precollected)
|
||||
AquariaLocationNames.OPEN_WATERS_TOP_RIGHT_AREA_TRANSTURTLE, precollected)
|
||||
self.__pre_fill_item(ItemNames.TRANSTURTLE_KELP_FOREST,
|
||||
AquariaLocationNames.KELP_FOREST_BOTTOM_LEFT_AREA_TRANSTURTLE,
|
||||
precollected)
|
||||
self.__pre_fill_item(ItemNames.TRANSTURTLE_HOME_WATERS, AquariaLocationNames.HOME_WATERS_TRANSTURTLE,
|
||||
precollected)
|
||||
self.__pre_fill_item(ItemNames.TRANSTURTLE_ABYSS, AquariaLocationNames.ABYSS_RIGHT_AREA_TRANSTURTLE,
|
||||
precollected)
|
||||
self.__pre_fill_item(ItemNames.TRANSTURTLE_BODY, AquariaLocationNames.FINAL_BOSS_AREA_TRANSTURTLE,
|
||||
precollected)
|
||||
AquariaLocationNames.KELP_FOREST_BOTTOM_LEFT_AREA_TRANSTURTLE, precollected)
|
||||
self.__pre_fill_item(ItemNames.TRANSTURTLE_HOME_WATERS, AquariaLocationNames.HOME_WATERS_TRANSTURTLE, precollected)
|
||||
if (self.options.objective.value != Objective.option_killing_the_four_gods and
|
||||
self.options.objective.value != Objective.option_gods_and_creator):
|
||||
self.__pre_fill_item(ItemNames.TRANSTURTLE_ABYSS, AquariaLocationNames.ABYSS_RIGHT_AREA_TRANSTURTLE,
|
||||
precollected)
|
||||
self.__pre_fill_item(ItemNames.TRANSTURTLE_BODY, AquariaLocationNames.FINAL_BOSS_AREA_TRANSTURTLE,
|
||||
precollected)
|
||||
# The last two are inverted because in the original game, they are special turtle that communicate directly
|
||||
self.__pre_fill_item(ItemNames.TRANSTURTLE_SIMON_SAYS, AquariaLocationNames.ARNASSI_RUINS_TRANSTURTLE,
|
||||
precollected, ItemClassification.progression)
|
||||
self.__pre_fill_item(ItemNames.TRANSTURTLE_ARNASSI_RUINS, AquariaLocationNames.SIMON_SAYS_AREA_TRANSTURTLE,
|
||||
precollected)
|
||||
if not self.options.throne_as_location:
|
||||
self.__pre_fill_item(ItemNames.DOOR_TO_CATHEDRAL, AquariaLocationNames.SITTING_ON_THRONE,
|
||||
precollected, ItemClassification.progression)
|
||||
for name, data in item_table.items():
|
||||
if name not in self.exclude:
|
||||
for i in range(data.count):
|
||||
for i in range(data.count):
|
||||
if name in self.exclude:
|
||||
self.exclude.remove(name)
|
||||
else:
|
||||
item = self.create_item(name)
|
||||
self.multiworld.itempool.append(item)
|
||||
|
||||
@@ -227,22 +243,41 @@ class AquariaWorld(World):
|
||||
self.ingredients_substitution.extend(dishes_substitution)
|
||||
|
||||
def fill_slot_data(self) -> Dict[str, Any]:
|
||||
"""
|
||||
Send some useful information to the client.
|
||||
"""
|
||||
return {"ingredientReplacement": self.ingredients_substitution,
|
||||
"aquarian_translate": bool(self.options.aquarian_translation.value),
|
||||
"blind_goal": bool(self.options.blind_goal.value),
|
||||
"secret_needed":
|
||||
self.options.objective.value == Objective.option_obtain_secrets_and_kill_the_creator,
|
||||
"goal": self.options.objective.value,
|
||||
"minibosses_to_kill": self.options.mini_bosses_to_beat.value,
|
||||
"bigbosses_to_kill": self.options.big_bosses_to_beat.value,
|
||||
"skip_first_vision": bool(self.options.skip_first_vision.value),
|
||||
"skip_final_boss_3rd_form": bool(self.options.skip_final_boss_3rd_form.value),
|
||||
"infinite_hot_soup": bool(self.options.infinite_hot_soup.value),
|
||||
"open_body_tongue": bool(self.options.open_body_tongue.value),
|
||||
"unconfine_home_water_energy_door":
|
||||
self.options.unconfine_home_water.value == UnconfineHomeWater.option_via_energy_door
|
||||
or self.options.unconfine_home_water.value == UnconfineHomeWater.option_via_both,
|
||||
"unconfine_home_water_transturtle":
|
||||
self.options.unconfine_home_water.value == UnconfineHomeWater.option_via_transturtle
|
||||
or self.options.unconfine_home_water.value == UnconfineHomeWater.option_via_both,
|
||||
"maximum_ingredient_amount": self.options.maximum_ingredient_amount.value,
|
||||
"bind_song_needed_to_get_under_rock_bulb": bool(self.options.bind_song_needed_to_get_under_rock_bulb),
|
||||
"no_progression_hard_or_hidden_locations": bool(self.options.no_progression_hard_or_hidden_locations),
|
||||
"light_needed_to_get_to_dark_places": bool(self.options.light_needed_to_get_to_dark_places),
|
||||
"turtle_randomizer": self.options.turtle_randomizer.value
|
||||
"turtle_randomizer": self.options.turtle_randomizer.value,
|
||||
"no_progression_simon_says": bool(self.options.no_progression_simon_says),
|
||||
"no_progression_kelp_forest": bool(self.options.no_progression_kelp_forest),
|
||||
"no_progression_veil": bool(self.options.no_progression_veil),
|
||||
"no_progression_mithalas": bool(self.options.no_progression_mithalas),
|
||||
"no_progression_energy_temple": bool(self.options.no_progression_energy_temple),
|
||||
"no_progression_arnassi_ruins": bool(self.options.no_progression_arnassi_ruins),
|
||||
"no_progression_frozen_veil": bool(self.options.no_progression_frozen_veil),
|
||||
"no_progression_abyss": bool(self.options.no_progression_abyss),
|
||||
"no_progression_sunken_city": bool(self.options.no_progression_sunken_city),
|
||||
"no_progression_body": bool(self.options.no_progression_body),
|
||||
"save_healing": bool(self.options.save_healing),
|
||||
"throne_as_location": bool(self.options.throne_as_location),
|
||||
"required_client_version": CLIENT_MINIMAL_COMPATIBILITY,
|
||||
}
|
||||
|
||||
185
worlds/aquaria/test/test_access_four_gods.py
Normal file
185
worlds/aquaria/test/test_access_four_gods.py
Normal file
@@ -0,0 +1,185 @@
|
||||
"""
|
||||
Author: Louis M
|
||||
Date: Thu, 18 Apr 2024 18:45:56 +0000
|
||||
Description: Unit test used to test accessibility of locations with the goal four gods
|
||||
"""
|
||||
|
||||
from . import AquariaTestBase
|
||||
from ..Items import ItemNames
|
||||
from ..Locations import AquariaLocationNames
|
||||
from ..Options import UnconfineHomeWater, Objective, BindSongNeededToGetUnderRockBulb
|
||||
|
||||
|
||||
class FourGodsAccessTest(AquariaTestBase):
|
||||
"""Unit test used to test accessibility of locations with the goal four gods"""
|
||||
options = {
|
||||
"objective": Objective.option_killing_the_four_gods,
|
||||
"bind_song_needed_to_get_under_rock_bulb": BindSongNeededToGetUnderRockBulb.option_true
|
||||
}
|
||||
|
||||
|
||||
def test_locations(self) -> None:
|
||||
"""Test locations with the goal four gods"""
|
||||
locations = [
|
||||
AquariaLocationNames.VERSE_CAVE_RIGHT_AREA_BIG_SEED,
|
||||
AquariaLocationNames.VERSE_CAVE_LEFT_AREA_BULB_UNDER_THE_ROCK_AT_THE_END_OF_THE_PATH,
|
||||
AquariaLocationNames.HOME_WATERS_BULB_UNDER_THE_ROCK_IN_THE_LEFT_PATH_FROM_THE_VERSE_CAVE,
|
||||
AquariaLocationNames.HOME_WATERS_BULB_IN_THE_PATH_BELOW_NAUTILUS_PRIME,
|
||||
AquariaLocationNames.HOME_WATERS_BULB_IN_THE_BOTTOM_LEFT_ROOM,
|
||||
AquariaLocationNames.HOME_WATERS_NAUTILUS_EGG,
|
||||
AquariaLocationNames.HOME_WATERS_TRANSTURTLE,
|
||||
AquariaLocationNames.NAIJA_S_HOME_BULB_AFTER_THE_ENERGY_DOOR,
|
||||
AquariaLocationNames.NAIJA_S_HOME_BULB_UNDER_THE_ROCK_AT_THE_RIGHT_OF_THE_MAIN_PATH,
|
||||
AquariaLocationNames.SONG_CAVE_BULB_UNDER_THE_ROCK_IN_THE_PATH_TO_THE_SINGING_STATUES,
|
||||
AquariaLocationNames.SONG_CAVE_BULB_UNDER_THE_ROCK_CLOSE_TO_THE_SONG_DOOR,
|
||||
AquariaLocationNames.SONG_CAVE_VERSE_EGG,
|
||||
AquariaLocationNames.SONG_CAVE_ANEMONE_SEED,
|
||||
AquariaLocationNames.ENERGY_TEMPLE_FIRST_AREA_BEATING_THE_ENERGY_STATUE,
|
||||
AquariaLocationNames.ENERGY_TEMPLE_FIRST_AREA_BULB_IN_THE_BOTTOM_ROOM_BLOCKED_BY_A_ROCK,
|
||||
AquariaLocationNames.ENERGY_TEMPLE_ENERGY_IDOL,
|
||||
AquariaLocationNames.ENERGY_TEMPLE_SECOND_AREA_BULB_UNDER_THE_ROCK,
|
||||
AquariaLocationNames.ENERGY_TEMPLE_BOTTOM_ENTRANCE_KROTITE_ARMOR,
|
||||
AquariaLocationNames.ENERGY_TEMPLE_THIRD_AREA_BULB_IN_THE_BOTTOM_PATH,
|
||||
AquariaLocationNames.ENERGY_TEMPLE_BOSS_AREA_FALLEN_GOD_TOOTH,
|
||||
AquariaLocationNames.ENERGY_TEMPLE_BLASTER_ROOM_BLASTER_EGG,
|
||||
AquariaLocationNames.OPEN_WATERS_TOP_LEFT_AREA_BULB_UNDER_THE_ROCK_IN_THE_RIGHT_PATH,
|
||||
AquariaLocationNames.OPEN_WATERS_TOP_LEFT_AREA_BULB_UNDER_THE_ROCK_IN_THE_LEFT_PATH,
|
||||
AquariaLocationNames.OPEN_WATERS_TOP_LEFT_AREA_BULB_TO_THE_RIGHT_OF_THE_SAVE_CRYSTAL,
|
||||
AquariaLocationNames.OPEN_WATERS_TOP_RIGHT_AREA_BULB_IN_THE_SMALL_PATH_BEFORE_MITHALAS,
|
||||
AquariaLocationNames.OPEN_WATERS_TOP_RIGHT_AREA_BULB_IN_THE_PATH_FROM_THE_LEFT_ENTRANCE,
|
||||
AquariaLocationNames.OPEN_WATERS_TOP_RIGHT_AREA_BULB_IN_THE_CLEARING_CLOSE_TO_THE_BOTTOM_EXIT,
|
||||
AquariaLocationNames.OPEN_WATERS_TOP_RIGHT_AREA_BULB_IN_THE_BIG_CLEARING_CLOSE_TO_THE_SAVE_CRYSTAL,
|
||||
AquariaLocationNames.OPEN_WATERS_TOP_RIGHT_AREA_BULB_IN_THE_BIG_CLEARING_TO_THE_TOP_EXIT,
|
||||
AquariaLocationNames.OPEN_WATERS_TOP_RIGHT_AREA_BULB_IN_THE_TURTLE_ROOM,
|
||||
AquariaLocationNames.OPEN_WATERS_TOP_RIGHT_AREA_TRANSTURTLE,
|
||||
AquariaLocationNames.OPEN_WATERS_TOP_RIGHT_AREA_FIRST_URN_IN_THE_MITHALAS_EXIT,
|
||||
AquariaLocationNames.OPEN_WATERS_TOP_RIGHT_AREA_SECOND_URN_IN_THE_MITHALAS_EXIT,
|
||||
AquariaLocationNames.OPEN_WATERS_TOP_RIGHT_AREA_THIRD_URN_IN_THE_MITHALAS_EXIT,
|
||||
AquariaLocationNames.OPEN_WATERS_BOTTOM_LEFT_AREA_BULB_BEHIND_THE_CHOMPER_FISH,
|
||||
AquariaLocationNames.OPEN_WATERS_BOTTOM_LEFT_AREA_BULB_INSIDE_THE_LOWEST_FISH_PASS,
|
||||
AquariaLocationNames.OPEN_WATERS_SKELETON_PATH_BULB_CLOSE_TO_THE_RIGHT_EXIT,
|
||||
AquariaLocationNames.OPEN_WATERS_SKELETON_PATH_BULB_BEHIND_THE_CHOMPER_FISH,
|
||||
AquariaLocationNames.OPEN_WATERS_SKELETON_PATH_KING_SKULL,
|
||||
AquariaLocationNames.ARNASSI_RUINS_BULB_IN_THE_RIGHT_PART,
|
||||
AquariaLocationNames.ARNASSI_RUINS_BULB_IN_THE_LEFT_PART,
|
||||
AquariaLocationNames.ARNASSI_RUINS_BULB_IN_THE_CENTER_PART,
|
||||
AquariaLocationNames.ARNASSI_RUINS_SONG_PLANT_SPORE,
|
||||
AquariaLocationNames.ARNASSI_RUINS_ARNASSI_ARMOR,
|
||||
AquariaLocationNames.ARNASSI_RUINS_ARNASSI_STATUE,
|
||||
AquariaLocationNames.ARNASSI_RUINS_TRANSTURTLE,
|
||||
AquariaLocationNames.ARNASSI_RUINS_CRAB_ARMOR,
|
||||
AquariaLocationNames.SIMON_SAYS_AREA_BEATING_SIMON_SAYS,
|
||||
AquariaLocationNames.SIMON_SAYS_AREA_TRANSTURTLE,
|
||||
AquariaLocationNames.MITHALAS_CITY_FIRST_BULB_IN_THE_LEFT_CITY_PART,
|
||||
AquariaLocationNames.MITHALAS_CITY_SECOND_BULB_IN_THE_LEFT_CITY_PART,
|
||||
AquariaLocationNames.MITHALAS_CITY_BULB_IN_THE_RIGHT_PART,
|
||||
AquariaLocationNames.MITHALAS_CITY_BULB_AT_THE_TOP_OF_THE_CITY,
|
||||
AquariaLocationNames.MITHALAS_CITY_FIRST_BULB_IN_A_BROKEN_HOME,
|
||||
AquariaLocationNames.MITHALAS_CITY_SECOND_BULB_IN_A_BROKEN_HOME,
|
||||
AquariaLocationNames.MITHALAS_CITY_BULB_IN_THE_BOTTOM_LEFT_PART,
|
||||
AquariaLocationNames.MITHALAS_CITY_FIRST_BULB_IN_ONE_OF_THE_HOMES,
|
||||
AquariaLocationNames.MITHALAS_CITY_SECOND_BULB_IN_ONE_OF_THE_HOMES,
|
||||
AquariaLocationNames.MITHALAS_CITY_FIRST_URN_IN_ONE_OF_THE_HOMES,
|
||||
AquariaLocationNames.MITHALAS_CITY_SECOND_URN_IN_ONE_OF_THE_HOMES,
|
||||
AquariaLocationNames.MITHALAS_CITY_FIRST_URN_IN_THE_CITY_RESERVE,
|
||||
AquariaLocationNames.MITHALAS_CITY_SECOND_URN_IN_THE_CITY_RESERVE,
|
||||
AquariaLocationNames.MITHALAS_CITY_THIRD_URN_IN_THE_CITY_RESERVE,
|
||||
AquariaLocationNames.MITHALAS_CITY_FIRST_BULB_AT_THE_END_OF_THE_TOP_PATH,
|
||||
AquariaLocationNames.MITHALAS_CITY_SECOND_BULB_AT_THE_END_OF_THE_TOP_PATH,
|
||||
AquariaLocationNames.MITHALAS_CITY_BULB_IN_THE_TOP_PATH,
|
||||
AquariaLocationNames.MITHALAS_CITY_MITHALAS_POT,
|
||||
AquariaLocationNames.MITHALAS_CITY_URN_IN_THE_CASTLE_FLOWER_TUBE_ENTRANCE,
|
||||
AquariaLocationNames.MITHALAS_CITY_DOLL,
|
||||
AquariaLocationNames.MITHALAS_CITY_URN_INSIDE_A_HOME_FISH_PASS,
|
||||
AquariaLocationNames.MITHALAS_CITY_CASTLE_BULB_IN_THE_FLESH_HOLE,
|
||||
AquariaLocationNames.MITHALAS_CITY_CASTLE_BLUE_BANNER,
|
||||
AquariaLocationNames.MITHALAS_CITY_CASTLE_URN_IN_THE_BEDROOM,
|
||||
AquariaLocationNames.MITHALAS_CITY_CASTLE_FIRST_URN_OF_THE_SINGLE_LAMP_PATH,
|
||||
AquariaLocationNames.MITHALAS_CITY_CASTLE_SECOND_URN_OF_THE_SINGLE_LAMP_PATH,
|
||||
AquariaLocationNames.MITHALAS_CITY_CASTLE_URN_IN_THE_BOTTOM_ROOM,
|
||||
AquariaLocationNames.MITHALAS_CITY_CASTLE_FIRST_URN_ON_THE_ENTRANCE_PATH,
|
||||
AquariaLocationNames.MITHALAS_CITY_CASTLE_SECOND_URN_ON_THE_ENTRANCE_PATH,
|
||||
AquariaLocationNames.MITHALAS_CITY_CASTLE_BEATING_THE_PRIESTS,
|
||||
AquariaLocationNames.MITHALAS_CITY_CASTLE_TRIDENT_HEAD,
|
||||
AquariaLocationNames.MITHALAS_CATHEDRAL_BULB_IN_THE_FLESH_ROOM_WITH_FLEAS,
|
||||
AquariaLocationNames.MITHALAS_CATHEDRAL_MITHALAN_DRESS,
|
||||
AquariaLocationNames.MITHALAS_CATHEDRAL_FIRST_URN_IN_THE_TOP_RIGHT_ROOM,
|
||||
AquariaLocationNames.MITHALAS_CATHEDRAL_SECOND_URN_IN_THE_TOP_RIGHT_ROOM,
|
||||
AquariaLocationNames.MITHALAS_CATHEDRAL_THIRD_URN_IN_THE_TOP_RIGHT_ROOM,
|
||||
AquariaLocationNames.MITHALAS_CATHEDRAL_URN_BEHIND_THE_FLESH_VEIN,
|
||||
AquariaLocationNames.MITHALAS_CATHEDRAL_URN_IN_THE_TOP_LEFT_EYES_BOSS_ROOM,
|
||||
AquariaLocationNames.MITHALAS_CATHEDRAL_FIRST_URN_IN_THE_PATH_BEHIND_THE_FLESH_VEIN,
|
||||
AquariaLocationNames.MITHALAS_CATHEDRAL_SECOND_URN_IN_THE_PATH_BEHIND_THE_FLESH_VEIN,
|
||||
AquariaLocationNames.MITHALAS_CATHEDRAL_THIRD_URN_IN_THE_PATH_BEHIND_THE_FLESH_VEIN,
|
||||
AquariaLocationNames.MITHALAS_CATHEDRAL_FOURTH_URN_IN_THE_TOP_RIGHT_ROOM,
|
||||
AquariaLocationNames.MITHALAS_CATHEDRAL_URN_BELOW_THE_LEFT_ENTRANCE,
|
||||
AquariaLocationNames.MITHALAS_CATHEDRAL_FIRST_URN_IN_THE_BOTTOM_RIGHT_PATH,
|
||||
AquariaLocationNames.MITHALAS_CATHEDRAL_SECOND_URN_IN_THE_BOTTOM_RIGHT_PATH,
|
||||
AquariaLocationNames.CATHEDRAL_UNDERGROUND_BULB_IN_THE_CENTER_PART,
|
||||
AquariaLocationNames.CATHEDRAL_UNDERGROUND_FIRST_BULB_IN_THE_TOP_LEFT_PART,
|
||||
AquariaLocationNames.CATHEDRAL_UNDERGROUND_SECOND_BULB_IN_THE_TOP_LEFT_PART,
|
||||
AquariaLocationNames.CATHEDRAL_UNDERGROUND_THIRD_BULB_IN_THE_TOP_LEFT_PART,
|
||||
AquariaLocationNames.CATHEDRAL_UNDERGROUND_BULB_CLOSE_TO_THE_SAVE_CRYSTAL,
|
||||
AquariaLocationNames.CATHEDRAL_UNDERGROUND_BULB_IN_THE_BOTTOM_RIGHT_PATH,
|
||||
AquariaLocationNames.MITHALAS_BOSS_AREA_BEATING_MITHALAN_GOD,
|
||||
AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_BULB_IN_THE_BOTTOM_LEFT_CLEARING,
|
||||
AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_BULB_IN_THE_PATH_DOWN_FROM_THE_TOP_LEFT_CLEARING,
|
||||
AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_BULB_IN_THE_TOP_LEFT_CLEARING,
|
||||
AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_JELLY_EGG,
|
||||
AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_BULB_CLOSE_TO_THE_VERSE_EGG,
|
||||
AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_VERSE_EGG,
|
||||
AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BULB_UNDER_THE_ROCK_IN_THE_RIGHT_PATH,
|
||||
AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BULB_AT_THE_LEFT_OF_THE_CENTER_CLEARING,
|
||||
AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BULB_IN_THE_LEFT_PATH_S_BIG_ROOM,
|
||||
AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BULB_IN_THE_LEFT_PATH_S_SMALL_ROOM,
|
||||
AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BULB_AT_THE_TOP_OF_THE_CENTER_CLEARING,
|
||||
AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BLACK_PEARL,
|
||||
AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BULB_IN_THE_TOP_FISH_PASS,
|
||||
AquariaLocationNames.KELP_FOREST_BOTTOM_LEFT_AREA_TRANSTURTLE,
|
||||
AquariaLocationNames.KELP_FOREST_BOTTOM_LEFT_AREA_BULB_CLOSE_TO_THE_SPIRIT_CRYSTALS,
|
||||
AquariaLocationNames.KELP_FOREST_BOTTOM_LEFT_AREA_WALKER_BABY,
|
||||
AquariaLocationNames.KELP_FOREST_BOTTOM_LEFT_AREA_FISH_CAVE_PUZZLE,
|
||||
AquariaLocationNames.KELP_FOREST_BOTTOM_RIGHT_AREA_ODD_CONTAINER,
|
||||
AquariaLocationNames.KELP_FOREST_BOSS_AREA_BEATING_DRUNIAN_GOD,
|
||||
AquariaLocationNames.KELP_FOREST_BOSS_ROOM_BULB_AT_THE_BOTTOM_OF_THE_AREA,
|
||||
AquariaLocationNames.KELP_FOREST_SPRITE_CAVE_BULB_INSIDE_THE_FISH_PASS,
|
||||
AquariaLocationNames.KELP_FOREST_SPRITE_CAVE_BULB_IN_THE_SECOND_ROOM,
|
||||
AquariaLocationNames.KELP_FOREST_SPRITE_CAVE_SEED_BAG,
|
||||
AquariaLocationNames.MERMOG_CAVE_BULB_IN_THE_LEFT_PART_OF_THE_CAVE,
|
||||
AquariaLocationNames.MERMOG_CAVE_PIRANHA_EGG,
|
||||
AquariaLocationNames.THE_VEIL_TOP_LEFT_AREA_IN_LI_S_CAVE,
|
||||
AquariaLocationNames.THE_VEIL_TOP_LEFT_AREA_BULB_UNDER_THE_ROCK_IN_THE_TOP_RIGHT_PATH,
|
||||
AquariaLocationNames.THE_VEIL_TOP_LEFT_AREA_BULB_HIDDEN_BEHIND_THE_BLOCKING_ROCK,
|
||||
AquariaLocationNames.THE_VEIL_TOP_LEFT_AREA_TRANSTURTLE,
|
||||
AquariaLocationNames.THE_VEIL_TOP_LEFT_AREA_BULB_INSIDE_THE_FISH_PASS,
|
||||
AquariaLocationNames.TURTLE_CAVE_TURTLE_EGG,
|
||||
AquariaLocationNames.TURTLE_CAVE_BULB_IN_BUBBLE_CLIFF,
|
||||
AquariaLocationNames.TURTLE_CAVE_URCHIN_COSTUME,
|
||||
AquariaLocationNames.THE_VEIL_TOP_RIGHT_AREA_BULB_IN_THE_MIDDLE_OF_THE_WALL_JUMP_CLIFF,
|
||||
AquariaLocationNames.THE_VEIL_TOP_RIGHT_AREA_GOLDEN_STARFISH,
|
||||
AquariaLocationNames.THE_VEIL_TOP_RIGHT_AREA_BULB_AT_THE_TOP_OF_THE_WATERFALL,
|
||||
AquariaLocationNames.THE_VEIL_TOP_RIGHT_AREA_TRANSTURTLE,
|
||||
AquariaLocationNames.THE_VEIL_BOTTOM_AREA_BULB_IN_THE_LEFT_PATH,
|
||||
AquariaLocationNames.THE_VEIL_BOTTOM_AREA_BULB_IN_THE_SPIRIT_PATH,
|
||||
AquariaLocationNames.THE_VEIL_BOTTOM_AREA_VERSE_EGG,
|
||||
AquariaLocationNames.THE_VEIL_BOTTOM_AREA_STONE_HEAD,
|
||||
AquariaLocationNames.OCTOPUS_CAVE_DUMBO_EGG,
|
||||
AquariaLocationNames.OCTOPUS_CAVE_BULB_IN_THE_PATH_BELOW_THE_OCTOPUS_CAVE_PATH,
|
||||
AquariaLocationNames.SUN_TEMPLE_BULB_IN_THE_TOP_LEFT_PART,
|
||||
AquariaLocationNames.SUN_TEMPLE_BULB_IN_THE_TOP_RIGHT_PART,
|
||||
AquariaLocationNames.SUN_TEMPLE_BULB_AT_THE_TOP_OF_THE_HIGH_DARK_ROOM,
|
||||
AquariaLocationNames.SUN_TEMPLE_GOLDEN_GEAR,
|
||||
AquariaLocationNames.SUN_TEMPLE_FIRST_BULB_OF_THE_TEMPLE,
|
||||
AquariaLocationNames.SUN_TEMPLE_BULB_ON_THE_RIGHT_PART,
|
||||
AquariaLocationNames.SUN_TEMPLE_BULB_IN_THE_HIDDEN_ROOM_OF_THE_RIGHT_PART,
|
||||
AquariaLocationNames.SUN_TEMPLE_SUN_KEY,
|
||||
AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_FIRST_PATH_BULB,
|
||||
AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_SECOND_PATH_BULB,
|
||||
AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_FIRST_CLIFF_BULB,
|
||||
AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_SECOND_CLIFF_BULB,
|
||||
AquariaLocationNames.SUN_TEMPLE_BOSS_AREA_BEATING_LUMEREAN_GOD,
|
||||
]
|
||||
items = [[ItemNames.ENERGY_FORM, ItemNames.SUN_FORM, ItemNames.BEAST_FORM, ItemNames.SPIRIT_FORM,
|
||||
ItemNames.FISH_FORM, ItemNames.HOT_SOUP, ItemNames.BIND_SONG, ItemNames.NATURE_FORM,
|
||||
ItemNames.DUAL_FORM]]
|
||||
self.assertAccessDependency(locations, items, True)
|
||||
35
worlds/aquaria/test/test_beast_or_soup_access.py
Normal file
35
worlds/aquaria/test/test_beast_or_soup_access.py
Normal file
@@ -0,0 +1,35 @@
|
||||
"""
|
||||
Author: Louis M
|
||||
Date: Thu, 18 Apr 2024 18:45:56 +0000
|
||||
Description: Unit test used to test accessibility of locations with and without the beast form or hot soup
|
||||
"""
|
||||
|
||||
from . import AquariaTestBase
|
||||
from ..Locations import AquariaLocationNames
|
||||
from ..Items import ItemNames
|
||||
|
||||
|
||||
class BeastOrSoupAccessTest(AquariaTestBase):
|
||||
"""Unit test used to test accessibility of locations with and without the beast form or hot soup"""
|
||||
|
||||
def test_beast_or_soup_location(self) -> None:
|
||||
"""Test locations that require beast form or hot soup"""
|
||||
locations = [
|
||||
AquariaLocationNames.MERMOG_CAVE_PIRANHA_EGG,
|
||||
AquariaLocationNames.MITHALAS_CATHEDRAL_MITHALAN_DRESS,
|
||||
AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_JELLY_EGG,
|
||||
AquariaLocationNames.TURTLE_CAVE_URCHIN_COSTUME,
|
||||
AquariaLocationNames.THE_VEIL_TOP_RIGHT_AREA_BULB_AT_THE_TOP_OF_THE_WATERFALL,
|
||||
AquariaLocationNames.SUNKEN_CITY_BULB_ON_TOP_OF_THE_BOSS_AREA,
|
||||
AquariaLocationNames.OCTOPUS_CAVE_DUMBO_EGG,
|
||||
AquariaLocationNames.BUBBLE_CAVE_BULB_IN_THE_LEFT_CAVE_WALL,
|
||||
AquariaLocationNames.BUBBLE_CAVE_BULB_IN_THE_RIGHT_CAVE_WALL_BEHIND_THE_ICE_CRYSTAL,
|
||||
AquariaLocationNames.BUBBLE_CAVE_VERSE_EGG,
|
||||
AquariaLocationNames.BEATING_MANTIS_SHRIMP_PRIME,
|
||||
AquariaLocationNames.BEATING_THE_GOLEM,
|
||||
AquariaLocationNames.BEATING_MERGOG,
|
||||
AquariaLocationNames.BEATING_OCTOPUS_PRIME,
|
||||
AquariaLocationNames.SUNKEN_CITY_CLEARED
|
||||
]
|
||||
items = [[ItemNames.BEAST_FORM, ItemNames.HOT_SOUP, ItemNames.HOT_SOUP_X_2]]
|
||||
self.assertAccessDependency(locations, items)
|
||||
@@ -35,6 +35,7 @@ class BindSongAccessTest(AquariaTestBase):
|
||||
AquariaLocationNames.ENERGY_TEMPLE_THIRD_AREA_BULB_IN_THE_BOTTOM_PATH,
|
||||
AquariaLocationNames.ENERGY_TEMPLE_BOSS_AREA_FALLEN_GOD_TOOTH,
|
||||
AquariaLocationNames.ENERGY_TEMPLE_BLASTER_ROOM_BLASTER_EGG,
|
||||
AquariaLocationNames.SITTING_ON_THRONE,
|
||||
*after_home_water_locations
|
||||
]
|
||||
items = [[ItemNames.BIND_SONG]]
|
||||
|
||||
@@ -38,6 +38,7 @@ class BindSongOptionAccessTest(AquariaTestBase):
|
||||
AquariaLocationNames.ENERGY_TEMPLE_THIRD_AREA_BULB_IN_THE_BOTTOM_PATH,
|
||||
AquariaLocationNames.ENERGY_TEMPLE_BOSS_AREA_FALLEN_GOD_TOOTH,
|
||||
AquariaLocationNames.ENERGY_TEMPLE_BLASTER_ROOM_BLASTER_EGG,
|
||||
AquariaLocationNames.SITTING_ON_THRONE,
|
||||
*after_home_water_locations
|
||||
]
|
||||
items = [[ItemNames.BIND_SONG]]
|
||||
|
||||
44
worlds/aquaria/test/test_no_progression_arnassi_ruins.py
Normal file
44
worlds/aquaria/test/test_no_progression_arnassi_ruins.py
Normal file
@@ -0,0 +1,44 @@
|
||||
"""
|
||||
Author: Louis M
|
||||
Date: Fri, 03 May 2024 14:07:35 +0000
|
||||
Description: Unit test used to test that no progression items can be put in Arnassi Ruins when option enabled
|
||||
"""
|
||||
|
||||
from . import AquariaTestBase
|
||||
from BaseClasses import ItemClassification
|
||||
from ..Locations import AquariaLocationNames
|
||||
|
||||
|
||||
class NoProgressionArnassiRuinsTest(AquariaTestBase):
|
||||
"""Unit test used to test that no progression items can be put in Arnassi Ruins when option enabled"""
|
||||
options = {
|
||||
"no_progression_arnassi_ruins": True
|
||||
}
|
||||
|
||||
unfillable_locations = [
|
||||
AquariaLocationNames.ARNASSI_RUINS_BULB_IN_THE_RIGHT_PART,
|
||||
AquariaLocationNames.ARNASSI_RUINS_BULB_IN_THE_LEFT_PART,
|
||||
AquariaLocationNames.ARNASSI_RUINS_BULB_IN_THE_CENTER_PART,
|
||||
AquariaLocationNames.ARNASSI_RUINS_SONG_PLANT_SPORE,
|
||||
AquariaLocationNames.ARNASSI_RUINS_ARNASSI_ARMOR,
|
||||
AquariaLocationNames.ARNASSI_RUINS_ARNASSI_STATUE,
|
||||
AquariaLocationNames.ARNASSI_RUINS_TRANSTURTLE,
|
||||
AquariaLocationNames.ARNASSI_RUINS_CRAB_ARMOR,
|
||||
]
|
||||
|
||||
def test_no_progression_arnassi_ruins(self) -> None:
|
||||
"""
|
||||
Unit test used to test that no progression items can be put in Arnassi Ruins when option enabled
|
||||
"""
|
||||
for location in self.unfillable_locations:
|
||||
for item_name in self.world.item_names:
|
||||
item = self.get_item_by_name(item_name)
|
||||
if item.classification == ItemClassification.progression:
|
||||
self.assertFalse(
|
||||
self.world.get_location(location).can_fill(self.multiworld.state, item, False),
|
||||
"The location \"" + location + "\" can be filled with \"" + item_name + "\"")
|
||||
else:
|
||||
self.assertTrue(
|
||||
self.world.get_location(location).can_fill(self.multiworld.state, item, False),
|
||||
"The location \"" + location + "\" cannot be filled with \"" + item_name + "\"")
|
||||
|
||||
44
worlds/aquaria/test/test_no_progression_energy_temple.py
Normal file
44
worlds/aquaria/test/test_no_progression_energy_temple.py
Normal file
@@ -0,0 +1,44 @@
|
||||
"""
|
||||
Author: Louis M
|
||||
Date: Fri, 03 May 2024 14:07:35 +0000
|
||||
Description: Unit test used to test that no progression items can be put in Energy Temple when option enabled
|
||||
"""
|
||||
|
||||
from . import AquariaTestBase
|
||||
from BaseClasses import ItemClassification
|
||||
from ..Locations import AquariaLocationNames
|
||||
|
||||
|
||||
class NoProgressionEnergyTempleTest(AquariaTestBase):
|
||||
"""Unit test used to test that no progression items can be put in Energy Temple when option enabled"""
|
||||
options = {
|
||||
"no_progression_energy_temple": True
|
||||
}
|
||||
|
||||
unfillable_locations = [
|
||||
AquariaLocationNames.ENERGY_TEMPLE_FIRST_AREA_BEATING_THE_ENERGY_STATUE,
|
||||
AquariaLocationNames.ENERGY_TEMPLE_FIRST_AREA_BULB_IN_THE_BOTTOM_ROOM_BLOCKED_BY_A_ROCK,
|
||||
AquariaLocationNames.ENERGY_TEMPLE_ENERGY_IDOL,
|
||||
AquariaLocationNames.ENERGY_TEMPLE_SECOND_AREA_BULB_UNDER_THE_ROCK,
|
||||
AquariaLocationNames.ENERGY_TEMPLE_BOTTOM_ENTRANCE_KROTITE_ARMOR,
|
||||
AquariaLocationNames.ENERGY_TEMPLE_THIRD_AREA_BULB_IN_THE_BOTTOM_PATH,
|
||||
AquariaLocationNames.ENERGY_TEMPLE_BOSS_AREA_FALLEN_GOD_TOOTH,
|
||||
AquariaLocationNames.ENERGY_TEMPLE_BLASTER_ROOM_BLASTER_EGG,
|
||||
]
|
||||
|
||||
def test_no_progression_energy_temple(self) -> None:
|
||||
"""
|
||||
Unit test used to test that no progression items can be put in Energy Temple when option enabled
|
||||
"""
|
||||
for location in self.unfillable_locations:
|
||||
for item_name in self.world.item_names:
|
||||
item = self.get_item_by_name(item_name)
|
||||
if item.classification == ItemClassification.progression:
|
||||
self.assertFalse(
|
||||
self.world.get_location(location).can_fill(self.multiworld.state, item, False),
|
||||
"The location \"" + location + "\" can be filled with \"" + item_name + "\"")
|
||||
else:
|
||||
self.assertTrue(
|
||||
self.world.get_location(location).can_fill(self.multiworld.state, item, False),
|
||||
"The location \"" + location + "\" cannot be filled with \"" + item_name + "\"")
|
||||
|
||||
44
worlds/aquaria/test/test_no_progression_frozen_veil.py
Normal file
44
worlds/aquaria/test/test_no_progression_frozen_veil.py
Normal file
@@ -0,0 +1,44 @@
|
||||
"""
|
||||
Author: Louis M
|
||||
Date: Fri, 03 May 2024 14:07:35 +0000
|
||||
Description: Unit test used to test that no progression items can be put in Frozen Veil when option enabled
|
||||
"""
|
||||
|
||||
from . import AquariaTestBase
|
||||
from BaseClasses import ItemClassification
|
||||
from ..Locations import AquariaLocationNames
|
||||
|
||||
|
||||
class NoProgressionFrozenVeilTest(AquariaTestBase):
|
||||
"""Unit test used to test that no progression items can be put in Frozen Veil when option enabled"""
|
||||
options = {
|
||||
"no_progression_frozen_veil": True
|
||||
}
|
||||
|
||||
unfillable_locations = [
|
||||
AquariaLocationNames.ICE_CAVERN_BULB_IN_THE_ROOM_TO_THE_RIGHT,
|
||||
AquariaLocationNames.ICE_CAVERN_FIRST_BULB_IN_THE_TOP_EXIT_ROOM,
|
||||
AquariaLocationNames.ICE_CAVERN_SECOND_BULB_IN_THE_TOP_EXIT_ROOM,
|
||||
AquariaLocationNames.ICE_CAVERN_THIRD_BULB_IN_THE_TOP_EXIT_ROOM,
|
||||
AquariaLocationNames.ICE_CAVERN_BULB_IN_THE_LEFT_ROOM,
|
||||
AquariaLocationNames.BUBBLE_CAVE_BULB_IN_THE_LEFT_CAVE_WALL,
|
||||
AquariaLocationNames.BUBBLE_CAVE_BULB_IN_THE_RIGHT_CAVE_WALL_BEHIND_THE_ICE_CRYSTAL,
|
||||
AquariaLocationNames.BUBBLE_CAVE_VERSE_EGG,
|
||||
]
|
||||
|
||||
def test_no_progression_frozen_veil(self) -> None:
|
||||
"""
|
||||
Unit test used to test that no progression items can be put in Frozen Veil when option enabled
|
||||
"""
|
||||
for location in self.unfillable_locations:
|
||||
for item_name in self.world.item_names:
|
||||
item = self.get_item_by_name(item_name)
|
||||
if item.classification == ItemClassification.progression:
|
||||
self.assertFalse(
|
||||
self.world.get_location(location).can_fill(self.multiworld.state, item, False),
|
||||
"The location \"" + location + "\" can be filled with \"" + item_name + "\"")
|
||||
else:
|
||||
self.assertTrue(
|
||||
self.world.get_location(location).can_fill(self.multiworld.state, item, False),
|
||||
"The location \"" + location + "\" cannot be filled with \"" + item_name + "\"")
|
||||
|
||||
@@ -9,7 +9,7 @@ from BaseClasses import ItemClassification
|
||||
from ..Locations import AquariaLocationNames
|
||||
|
||||
|
||||
class UNoProgressionHardHiddenTest(AquariaTestBase):
|
||||
class NoProgressionHardHiddenTest(AquariaTestBase):
|
||||
"""Unit test used to test that no progression items can be put in hard or hidden locations when option enabled"""
|
||||
options = {
|
||||
"no_progression_hard_or_hidden_locations": True
|
||||
@@ -43,7 +43,7 @@ class UNoProgressionHardHiddenTest(AquariaTestBase):
|
||||
AquariaLocationNames.ARNASSI_RUINS_ARNASSI_ARMOR,
|
||||
]
|
||||
|
||||
def test_unconfine_home_water_both_location_fillable(self) -> None:
|
||||
def test_no_progression_hard_or_hidden(self) -> None:
|
||||
"""
|
||||
Unit test used to test that no progression items can be put in hard or hidden locations when option enabled
|
||||
"""
|
||||
|
||||
61
worlds/aquaria/test/test_no_progression_kelp_forest.py
Normal file
61
worlds/aquaria/test/test_no_progression_kelp_forest.py
Normal file
@@ -0,0 +1,61 @@
|
||||
"""
|
||||
Author: Louis M
|
||||
Date: Fri, 03 May 2024 14:07:35 +0000
|
||||
Description: Unit test used to test that no progression items can be put in Kelp Forest when option enabled
|
||||
"""
|
||||
|
||||
from . import AquariaTestBase
|
||||
from BaseClasses import ItemClassification
|
||||
from ..Locations import AquariaLocationNames
|
||||
|
||||
|
||||
class NoProgressionKelpForestTest(AquariaTestBase):
|
||||
"""Unit test used to test that no progression items can be put in Kelp Forest when option enabled"""
|
||||
options = {
|
||||
"no_progression_kelp_forest": True
|
||||
}
|
||||
|
||||
unfillable_locations = [
|
||||
AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_BULB_IN_THE_BOTTOM_LEFT_CLEARING,
|
||||
AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_BULB_IN_THE_PATH_DOWN_FROM_THE_TOP_LEFT_CLEARING,
|
||||
AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_BULB_IN_THE_TOP_LEFT_CLEARING,
|
||||
AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_JELLY_EGG,
|
||||
AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_BULB_CLOSE_TO_THE_VERSE_EGG,
|
||||
AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_VERSE_EGG,
|
||||
AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BULB_UNDER_THE_ROCK_IN_THE_RIGHT_PATH,
|
||||
AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BULB_AT_THE_LEFT_OF_THE_CENTER_CLEARING,
|
||||
AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BULB_IN_THE_LEFT_PATH_S_BIG_ROOM,
|
||||
AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BULB_IN_THE_LEFT_PATH_S_SMALL_ROOM,
|
||||
AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BULB_AT_THE_TOP_OF_THE_CENTER_CLEARING,
|
||||
AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BLACK_PEARL,
|
||||
AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BULB_IN_THE_TOP_FISH_PASS,
|
||||
AquariaLocationNames.KELP_FOREST_BOTTOM_LEFT_AREA_BULB_CLOSE_TO_THE_SPIRIT_CRYSTALS,
|
||||
AquariaLocationNames.KELP_FOREST_BOTTOM_LEFT_AREA_WALKER_BABY,
|
||||
AquariaLocationNames.KELP_FOREST_BOTTOM_LEFT_AREA_TRANSTURTLE,
|
||||
AquariaLocationNames.KELP_FOREST_BOTTOM_RIGHT_AREA_ODD_CONTAINER,
|
||||
AquariaLocationNames.KELP_FOREST_BOSS_AREA_BEATING_DRUNIAN_GOD,
|
||||
AquariaLocationNames.KELP_FOREST_BOSS_ROOM_BULB_AT_THE_BOTTOM_OF_THE_AREA,
|
||||
AquariaLocationNames.KELP_FOREST_BOTTOM_LEFT_AREA_FISH_CAVE_PUZZLE,
|
||||
AquariaLocationNames.KELP_FOREST_SPRITE_CAVE_BULB_INSIDE_THE_FISH_PASS,
|
||||
AquariaLocationNames.KELP_FOREST_SPRITE_CAVE_BULB_IN_THE_SECOND_ROOM,
|
||||
AquariaLocationNames.KELP_FOREST_SPRITE_CAVE_SEED_BAG,
|
||||
AquariaLocationNames.MERMOG_CAVE_BULB_IN_THE_LEFT_PART_OF_THE_CAVE,
|
||||
AquariaLocationNames.MERMOG_CAVE_PIRANHA_EGG,
|
||||
]
|
||||
|
||||
def test_no_progression_kelp_forest(self) -> None:
|
||||
"""
|
||||
Unit test used to test that no progression items can be put in Kelp Forest when option enabled
|
||||
"""
|
||||
for location in self.unfillable_locations:
|
||||
for item_name in self.world.item_names:
|
||||
item = self.get_item_by_name(item_name)
|
||||
if item.classification == ItemClassification.progression:
|
||||
self.assertFalse(
|
||||
self.world.get_location(location).can_fill(self.multiworld.state, item, False),
|
||||
"The location \"" + location + "\" can be filled with \"" + item_name + "\"")
|
||||
else:
|
||||
self.assertTrue(
|
||||
self.world.get_location(location).can_fill(self.multiworld.state, item, False),
|
||||
"The location \"" + location + "\" cannot be filled with \"" + item_name + "\"")
|
||||
|
||||
88
worlds/aquaria/test/test_no_progression_mithalas.py
Normal file
88
worlds/aquaria/test/test_no_progression_mithalas.py
Normal file
@@ -0,0 +1,88 @@
|
||||
"""
|
||||
Author: Louis M
|
||||
Date: Fri, 03 May 2024 14:07:35 +0000
|
||||
Description: Unit test used to test that no progression items can be put in Mithalas when option enabled
|
||||
"""
|
||||
|
||||
from . import AquariaTestBase
|
||||
from BaseClasses import ItemClassification
|
||||
from ..Locations import AquariaLocationNames
|
||||
|
||||
|
||||
class NoProgressionMithalasTest(AquariaTestBase):
|
||||
"""Unit test used to test that no progression items can be put in Mithalas when option enabled"""
|
||||
options = {
|
||||
"no_progression_mithalas": True
|
||||
}
|
||||
|
||||
unfillable_locations = [
|
||||
AquariaLocationNames.MITHALAS_CITY_FIRST_BULB_IN_THE_LEFT_CITY_PART,
|
||||
AquariaLocationNames.MITHALAS_CITY_SECOND_BULB_IN_THE_LEFT_CITY_PART,
|
||||
AquariaLocationNames.MITHALAS_CITY_BULB_IN_THE_RIGHT_PART,
|
||||
AquariaLocationNames.MITHALAS_CITY_BULB_AT_THE_TOP_OF_THE_CITY,
|
||||
AquariaLocationNames.MITHALAS_CITY_FIRST_BULB_IN_A_BROKEN_HOME,
|
||||
AquariaLocationNames.MITHALAS_CITY_SECOND_BULB_IN_A_BROKEN_HOME,
|
||||
AquariaLocationNames.MITHALAS_CITY_BULB_IN_THE_BOTTOM_LEFT_PART,
|
||||
AquariaLocationNames.MITHALAS_CITY_FIRST_BULB_IN_ONE_OF_THE_HOMES,
|
||||
AquariaLocationNames.MITHALAS_CITY_SECOND_BULB_IN_ONE_OF_THE_HOMES,
|
||||
AquariaLocationNames.MITHALAS_CITY_FIRST_URN_IN_ONE_OF_THE_HOMES,
|
||||
AquariaLocationNames.MITHALAS_CITY_SECOND_URN_IN_ONE_OF_THE_HOMES,
|
||||
AquariaLocationNames.MITHALAS_CITY_FIRST_URN_IN_THE_CITY_RESERVE,
|
||||
AquariaLocationNames.MITHALAS_CITY_SECOND_URN_IN_THE_CITY_RESERVE,
|
||||
AquariaLocationNames.MITHALAS_CITY_THIRD_URN_IN_THE_CITY_RESERVE,
|
||||
AquariaLocationNames.MITHALAS_CITY_FIRST_BULB_AT_THE_END_OF_THE_TOP_PATH,
|
||||
AquariaLocationNames.MITHALAS_CITY_SECOND_BULB_AT_THE_END_OF_THE_TOP_PATH,
|
||||
AquariaLocationNames.MITHALAS_CITY_BULB_IN_THE_TOP_PATH,
|
||||
AquariaLocationNames.MITHALAS_CITY_MITHALAS_POT,
|
||||
AquariaLocationNames.MITHALAS_CITY_URN_IN_THE_CASTLE_FLOWER_TUBE_ENTRANCE,
|
||||
AquariaLocationNames.MITHALAS_CITY_DOLL,
|
||||
AquariaLocationNames.MITHALAS_CITY_URN_INSIDE_A_HOME_FISH_PASS,
|
||||
AquariaLocationNames.MITHALAS_CITY_CASTLE_BULB_IN_THE_FLESH_HOLE,
|
||||
AquariaLocationNames.MITHALAS_CITY_CASTLE_BLUE_BANNER,
|
||||
AquariaLocationNames.MITHALAS_CITY_CASTLE_URN_IN_THE_BEDROOM,
|
||||
AquariaLocationNames.MITHALAS_CITY_CASTLE_FIRST_URN_OF_THE_SINGLE_LAMP_PATH,
|
||||
AquariaLocationNames.MITHALAS_CITY_CASTLE_SECOND_URN_OF_THE_SINGLE_LAMP_PATH,
|
||||
AquariaLocationNames.MITHALAS_CITY_CASTLE_URN_IN_THE_BOTTOM_ROOM,
|
||||
AquariaLocationNames.MITHALAS_CITY_CASTLE_FIRST_URN_ON_THE_ENTRANCE_PATH,
|
||||
AquariaLocationNames.MITHALAS_CITY_CASTLE_SECOND_URN_ON_THE_ENTRANCE_PATH,
|
||||
AquariaLocationNames.MITHALAS_CITY_CASTLE_BEATING_THE_PRIESTS,
|
||||
AquariaLocationNames.MITHALAS_CITY_CASTLE_TRIDENT_HEAD,
|
||||
AquariaLocationNames.MITHALAS_CATHEDRAL_FIRST_URN_IN_THE_TOP_RIGHT_ROOM,
|
||||
AquariaLocationNames.MITHALAS_CATHEDRAL_SECOND_URN_IN_THE_TOP_RIGHT_ROOM,
|
||||
AquariaLocationNames.MITHALAS_CATHEDRAL_THIRD_URN_IN_THE_TOP_RIGHT_ROOM,
|
||||
AquariaLocationNames.MITHALAS_CATHEDRAL_BULB_IN_THE_FLESH_ROOM_WITH_FLEAS,
|
||||
AquariaLocationNames.MITHALAS_CATHEDRAL_FIRST_URN_IN_THE_BOTTOM_RIGHT_PATH,
|
||||
AquariaLocationNames.MITHALAS_CATHEDRAL_SECOND_URN_IN_THE_BOTTOM_RIGHT_PATH,
|
||||
AquariaLocationNames.MITHALAS_CATHEDRAL_URN_BEHIND_THE_FLESH_VEIN,
|
||||
AquariaLocationNames.MITHALAS_CATHEDRAL_URN_IN_THE_TOP_LEFT_EYES_BOSS_ROOM,
|
||||
AquariaLocationNames.MITHALAS_CATHEDRAL_FIRST_URN_IN_THE_PATH_BEHIND_THE_FLESH_VEIN,
|
||||
AquariaLocationNames.MITHALAS_CATHEDRAL_SECOND_URN_IN_THE_PATH_BEHIND_THE_FLESH_VEIN,
|
||||
AquariaLocationNames.MITHALAS_CATHEDRAL_THIRD_URN_IN_THE_PATH_BEHIND_THE_FLESH_VEIN,
|
||||
AquariaLocationNames.MITHALAS_CATHEDRAL_FOURTH_URN_IN_THE_TOP_RIGHT_ROOM,
|
||||
AquariaLocationNames.MITHALAS_CATHEDRAL_MITHALAN_DRESS,
|
||||
AquariaLocationNames.MITHALAS_CATHEDRAL_URN_BELOW_THE_LEFT_ENTRANCE,
|
||||
AquariaLocationNames.CATHEDRAL_UNDERGROUND_BULB_IN_THE_CENTER_PART,
|
||||
AquariaLocationNames.CATHEDRAL_UNDERGROUND_FIRST_BULB_IN_THE_TOP_LEFT_PART,
|
||||
AquariaLocationNames.CATHEDRAL_UNDERGROUND_SECOND_BULB_IN_THE_TOP_LEFT_PART,
|
||||
AquariaLocationNames.CATHEDRAL_UNDERGROUND_THIRD_BULB_IN_THE_TOP_LEFT_PART,
|
||||
AquariaLocationNames.CATHEDRAL_UNDERGROUND_BULB_CLOSE_TO_THE_SAVE_CRYSTAL,
|
||||
AquariaLocationNames.CATHEDRAL_UNDERGROUND_BULB_IN_THE_BOTTOM_RIGHT_PATH,
|
||||
AquariaLocationNames.MITHALAS_BOSS_AREA_BEATING_MITHALAN_GOD,
|
||||
]
|
||||
|
||||
def test_no_progression_mithalas(self) -> None:
|
||||
"""
|
||||
Unit test used to test that no progression items can be put in Mithalas when option enabled
|
||||
"""
|
||||
for location in self.unfillable_locations:
|
||||
for item_name in self.world.item_names:
|
||||
item = self.get_item_by_name(item_name)
|
||||
if item.classification == ItemClassification.progression:
|
||||
self.assertFalse(
|
||||
self.world.get_location(location).can_fill(self.multiworld.state, item, False),
|
||||
"The location \"" + location + "\" can be filled with \"" + item_name + "\"")
|
||||
else:
|
||||
self.assertTrue(
|
||||
self.world.get_location(location).can_fill(self.multiworld.state, item, False),
|
||||
"The location \"" + location + "\" cannot be filled with \"" + item_name + "\"")
|
||||
|
||||
38
worlds/aquaria/test/test_no_progression_simon_says.py
Normal file
38
worlds/aquaria/test/test_no_progression_simon_says.py
Normal file
@@ -0,0 +1,38 @@
|
||||
"""
|
||||
Author: Louis M
|
||||
Date: Fri, 03 May 2024 14:07:35 +0000
|
||||
Description: Unit test used to test that no progression items can be put in Simon says when option enabled
|
||||
"""
|
||||
|
||||
from . import AquariaTestBase
|
||||
from BaseClasses import ItemClassification
|
||||
from ..Locations import AquariaLocationNames
|
||||
|
||||
|
||||
class NoProgressionSimonSaysTest(AquariaTestBase):
|
||||
"""Unit test used to test that no progression items can be put in Simon says when option enabled"""
|
||||
options = {
|
||||
"no_progression_simon_says": True
|
||||
}
|
||||
|
||||
unfillable_locations = [
|
||||
AquariaLocationNames.SIMON_SAYS_AREA_BEATING_SIMON_SAYS,
|
||||
AquariaLocationNames.SIMON_SAYS_AREA_TRANSTURTLE,
|
||||
]
|
||||
|
||||
def test_no_progression_simon_says(self) -> None:
|
||||
"""
|
||||
Unit test used to test that no progression items can be put in Simon says when option enabled
|
||||
"""
|
||||
for location in self.unfillable_locations:
|
||||
for item_name in self.world.item_names:
|
||||
item = self.get_item_by_name(item_name)
|
||||
if item.classification == ItemClassification.progression:
|
||||
self.assertFalse(
|
||||
self.world.get_location(location).can_fill(self.multiworld.state, item, False),
|
||||
"The location \"" + location + "\" can be filled with \"" + item_name + "\"")
|
||||
else:
|
||||
self.assertTrue(
|
||||
self.world.get_location(location).can_fill(self.multiworld.state, item, False),
|
||||
"The location \"" + location + "\" cannot be filled with \"" + item_name + "\"")
|
||||
|
||||
67
worlds/aquaria/test/test_no_progression_veil.py
Normal file
67
worlds/aquaria/test/test_no_progression_veil.py
Normal file
@@ -0,0 +1,67 @@
|
||||
"""
|
||||
Author: Louis M
|
||||
Date: Fri, 03 May 2024 14:07:35 +0000
|
||||
Description: Unit test used to test that no progression items can be put in the Veil when option enabled
|
||||
"""
|
||||
|
||||
from . import AquariaTestBase
|
||||
from BaseClasses import ItemClassification
|
||||
from ..Locations import AquariaLocationNames
|
||||
|
||||
|
||||
class NoProgressionVeilTest(AquariaTestBase):
|
||||
"""Unit test used to test that no progression items can be put in the Veil when option enabled"""
|
||||
options = {
|
||||
"no_progression_veil": True
|
||||
}
|
||||
|
||||
unfillable_locations = [
|
||||
AquariaLocationNames.THE_VEIL_TOP_LEFT_AREA_IN_LI_S_CAVE,
|
||||
AquariaLocationNames.THE_VEIL_TOP_LEFT_AREA_BULB_UNDER_THE_ROCK_IN_THE_TOP_RIGHT_PATH,
|
||||
AquariaLocationNames.THE_VEIL_TOP_LEFT_AREA_BULB_HIDDEN_BEHIND_THE_BLOCKING_ROCK,
|
||||
AquariaLocationNames.THE_VEIL_TOP_LEFT_AREA_TRANSTURTLE,
|
||||
AquariaLocationNames.THE_VEIL_TOP_LEFT_AREA_BULB_INSIDE_THE_FISH_PASS,
|
||||
AquariaLocationNames.TURTLE_CAVE_TURTLE_EGG,
|
||||
AquariaLocationNames.TURTLE_CAVE_BULB_IN_BUBBLE_CLIFF,
|
||||
AquariaLocationNames.TURTLE_CAVE_URCHIN_COSTUME,
|
||||
AquariaLocationNames.THE_VEIL_TOP_RIGHT_AREA_BULB_IN_THE_MIDDLE_OF_THE_WALL_JUMP_CLIFF,
|
||||
AquariaLocationNames.THE_VEIL_TOP_RIGHT_AREA_GOLDEN_STARFISH,
|
||||
AquariaLocationNames.THE_VEIL_TOP_RIGHT_AREA_BULB_AT_THE_TOP_OF_THE_WATERFALL,
|
||||
AquariaLocationNames.THE_VEIL_TOP_RIGHT_AREA_TRANSTURTLE,
|
||||
AquariaLocationNames.THE_VEIL_BOTTOM_AREA_BULB_IN_THE_LEFT_PATH,
|
||||
AquariaLocationNames.THE_VEIL_BOTTOM_AREA_BULB_IN_THE_SPIRIT_PATH,
|
||||
AquariaLocationNames.THE_VEIL_BOTTOM_AREA_VERSE_EGG,
|
||||
AquariaLocationNames.THE_VEIL_BOTTOM_AREA_STONE_HEAD,
|
||||
AquariaLocationNames.OCTOPUS_CAVE_DUMBO_EGG,
|
||||
AquariaLocationNames.OCTOPUS_CAVE_BULB_IN_THE_PATH_BELOW_THE_OCTOPUS_CAVE_PATH,
|
||||
AquariaLocationNames.SUN_TEMPLE_BULB_IN_THE_TOP_LEFT_PART,
|
||||
AquariaLocationNames.SUN_TEMPLE_BULB_IN_THE_TOP_RIGHT_PART,
|
||||
AquariaLocationNames.SUN_TEMPLE_BULB_AT_THE_TOP_OF_THE_HIGH_DARK_ROOM,
|
||||
AquariaLocationNames.SUN_TEMPLE_GOLDEN_GEAR,
|
||||
AquariaLocationNames.SUN_TEMPLE_FIRST_BULB_OF_THE_TEMPLE,
|
||||
AquariaLocationNames.SUN_TEMPLE_BULB_ON_THE_RIGHT_PART,
|
||||
AquariaLocationNames.SUN_TEMPLE_BULB_IN_THE_HIDDEN_ROOM_OF_THE_RIGHT_PART,
|
||||
AquariaLocationNames.SUN_TEMPLE_SUN_KEY,
|
||||
AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_FIRST_PATH_BULB,
|
||||
AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_SECOND_PATH_BULB,
|
||||
AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_FIRST_CLIFF_BULB,
|
||||
AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_SECOND_CLIFF_BULB,
|
||||
AquariaLocationNames.SUN_TEMPLE_BOSS_AREA_BEATING_LUMEREAN_GOD,
|
||||
]
|
||||
|
||||
def test_no_progression_veil(self) -> None:
|
||||
"""
|
||||
Unit test used to test that no progression items can be put in the Veil when option enabled
|
||||
"""
|
||||
for location in self.unfillable_locations:
|
||||
for item_name in self.world.item_names:
|
||||
item = self.get_item_by_name(item_name)
|
||||
if item.classification == ItemClassification.progression:
|
||||
self.assertFalse(
|
||||
self.world.get_location(location).can_fill(self.multiworld.state, item, False),
|
||||
"The location \"" + location + "\" can be filled with \"" + item_name + "\"")
|
||||
else:
|
||||
self.assertTrue(
|
||||
self.world.get_location(location).can_fill(self.multiworld.state, item, False),
|
||||
"The location \"" + location + "\" cannot be filled with \"" + item_name + "\"")
|
||||
|
||||
37
worlds/aquaria/test/test_progression_arnassi_ruins.py
Normal file
37
worlds/aquaria/test/test_progression_arnassi_ruins.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""
|
||||
Author: Louis M
|
||||
Date: Fri, 03 May 2024 14:07:35 +0000
|
||||
Description: Unit test used to test that progression items can be put in Arnassi Ruins area when option enabled
|
||||
"""
|
||||
|
||||
from . import AquariaTestBase
|
||||
from ..Locations import AquariaLocationNames
|
||||
|
||||
|
||||
class ProgressionArnassiRuinsTest(AquariaTestBase):
|
||||
"""Unit test used to test that progression items can be put in Arnassi Ruins area when option enabled"""
|
||||
options = {
|
||||
"no_progression_arnassi_ruins": False
|
||||
}
|
||||
|
||||
unfillable_locations = [
|
||||
AquariaLocationNames.ARNASSI_RUINS_BULB_IN_THE_RIGHT_PART,
|
||||
AquariaLocationNames.ARNASSI_RUINS_BULB_IN_THE_LEFT_PART,
|
||||
AquariaLocationNames.ARNASSI_RUINS_BULB_IN_THE_CENTER_PART,
|
||||
AquariaLocationNames.ARNASSI_RUINS_SONG_PLANT_SPORE,
|
||||
AquariaLocationNames.ARNASSI_RUINS_ARNASSI_ARMOR,
|
||||
AquariaLocationNames.ARNASSI_RUINS_ARNASSI_STATUE,
|
||||
AquariaLocationNames.ARNASSI_RUINS_TRANSTURTLE,
|
||||
AquariaLocationNames.ARNASSI_RUINS_CRAB_ARMOR,
|
||||
]
|
||||
|
||||
def test_progression_arnassi_ruins(self) -> None:
|
||||
"""
|
||||
Unit test used to test that progression items can be put in Arnassi Ruins area when option enabled
|
||||
"""
|
||||
for location in self.unfillable_locations:
|
||||
for item_name in self.world.item_names:
|
||||
item = self.get_item_by_name(item_name)
|
||||
self.assertTrue(
|
||||
self.world.get_location(location).can_fill(self.multiworld.state, item, False),
|
||||
"The location \"" + location + "\" cannot be filled with \"" + item_name + "\"")
|
||||
37
worlds/aquaria/test/test_progression_energy_temple.py
Normal file
37
worlds/aquaria/test/test_progression_energy_temple.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""
|
||||
Author: Louis M
|
||||
Date: Fri, 03 May 2024 14:07:35 +0000
|
||||
Description: Unit test used to test that progression items can be put in Energy Temple area when option enabled
|
||||
"""
|
||||
|
||||
from . import AquariaTestBase
|
||||
from ..Locations import AquariaLocationNames
|
||||
|
||||
|
||||
class ProgressionEnergyTempleTest(AquariaTestBase):
|
||||
"""Unit test used to test that progression items can be put in Energy Temple area when option enabled"""
|
||||
options = {
|
||||
"no_progression_energy_temple": False
|
||||
}
|
||||
|
||||
unfillable_locations = [
|
||||
AquariaLocationNames.ENERGY_TEMPLE_FIRST_AREA_BEATING_THE_ENERGY_STATUE,
|
||||
AquariaLocationNames.ENERGY_TEMPLE_FIRST_AREA_BULB_IN_THE_BOTTOM_ROOM_BLOCKED_BY_A_ROCK,
|
||||
AquariaLocationNames.ENERGY_TEMPLE_ENERGY_IDOL,
|
||||
AquariaLocationNames.ENERGY_TEMPLE_SECOND_AREA_BULB_UNDER_THE_ROCK,
|
||||
AquariaLocationNames.ENERGY_TEMPLE_BOTTOM_ENTRANCE_KROTITE_ARMOR,
|
||||
AquariaLocationNames.ENERGY_TEMPLE_THIRD_AREA_BULB_IN_THE_BOTTOM_PATH,
|
||||
AquariaLocationNames.ENERGY_TEMPLE_BOSS_AREA_FALLEN_GOD_TOOTH,
|
||||
AquariaLocationNames.ENERGY_TEMPLE_BLASTER_ROOM_BLASTER_EGG,
|
||||
]
|
||||
|
||||
def test_progression_energy_temple(self) -> None:
|
||||
"""
|
||||
Unit test used to test that progression items can be put in Energy Temple area when option enabled
|
||||
"""
|
||||
for location in self.unfillable_locations:
|
||||
for item_name in self.world.item_names:
|
||||
item = self.get_item_by_name(item_name)
|
||||
self.assertTrue(
|
||||
self.world.get_location(location).can_fill(self.multiworld.state, item, False),
|
||||
"The location \"" + location + "\" cannot be filled with \"" + item_name + "\"")
|
||||
37
worlds/aquaria/test/test_progression_frozen_veil.py
Normal file
37
worlds/aquaria/test/test_progression_frozen_veil.py
Normal file
@@ -0,0 +1,37 @@
|
||||
"""
|
||||
Author: Louis M
|
||||
Date: Fri, 03 May 2024 14:07:35 +0000
|
||||
Description: Unit test used to test that progression items can be put in Frozen Veil area when option enabled
|
||||
"""
|
||||
|
||||
from . import AquariaTestBase
|
||||
from ..Locations import AquariaLocationNames
|
||||
|
||||
|
||||
class ProgressionFrozenVeilTest(AquariaTestBase):
|
||||
"""Unit test used to test that progression items can be put in Frozen Veil area when option enabled"""
|
||||
options = {
|
||||
"no_progression_frozen_veil": False
|
||||
}
|
||||
|
||||
unfillable_locations = [
|
||||
AquariaLocationNames.ICE_CAVERN_BULB_IN_THE_ROOM_TO_THE_RIGHT,
|
||||
AquariaLocationNames.ICE_CAVERN_FIRST_BULB_IN_THE_TOP_EXIT_ROOM,
|
||||
AquariaLocationNames.ICE_CAVERN_SECOND_BULB_IN_THE_TOP_EXIT_ROOM,
|
||||
AquariaLocationNames.ICE_CAVERN_THIRD_BULB_IN_THE_TOP_EXIT_ROOM,
|
||||
AquariaLocationNames.ICE_CAVERN_BULB_IN_THE_LEFT_ROOM,
|
||||
AquariaLocationNames.BUBBLE_CAVE_BULB_IN_THE_LEFT_CAVE_WALL,
|
||||
AquariaLocationNames.BUBBLE_CAVE_BULB_IN_THE_RIGHT_CAVE_WALL_BEHIND_THE_ICE_CRYSTAL,
|
||||
AquariaLocationNames.BUBBLE_CAVE_VERSE_EGG,
|
||||
]
|
||||
|
||||
def test_progression_frozen_veil(self) -> None:
|
||||
"""
|
||||
Unit test used to test that progression items can be put in Frozen Veil area when option enabled
|
||||
"""
|
||||
for location in self.unfillable_locations:
|
||||
for item_name in self.world.item_names:
|
||||
item = self.get_item_by_name(item_name)
|
||||
self.assertTrue(
|
||||
self.world.get_location(location).can_fill(self.multiworld.state, item, False),
|
||||
"The location \"" + location + "\" cannot be filled with \"" + item_name + "\"")
|
||||
@@ -8,7 +8,7 @@ from . import AquariaTestBase
|
||||
from ..Locations import AquariaLocationNames
|
||||
|
||||
|
||||
class UNoProgressionHardHiddenTest(AquariaTestBase):
|
||||
class ProgressionHardHiddenTest(AquariaTestBase):
|
||||
"""Unit test used to test that no progression items can be put in hard or hidden locations when option disabled"""
|
||||
options = {
|
||||
"no_progression_hard_or_hidden_locations": False
|
||||
@@ -42,7 +42,7 @@ class UNoProgressionHardHiddenTest(AquariaTestBase):
|
||||
AquariaLocationNames.ARNASSI_RUINS_ARNASSI_ARMOR,
|
||||
]
|
||||
|
||||
def test_unconfine_home_water_both_location_fillable(self) -> None:
|
||||
def test_progression_hard_or_hidden(self) -> None:
|
||||
"""Unit test used to test that progression items can be put in hard or hidden locations when option disabled"""
|
||||
for location in self.unfillable_locations:
|
||||
for item_name in self.world.item_names:
|
||||
|
||||
54
worlds/aquaria/test/test_progression_kelp_forest.py
Normal file
54
worlds/aquaria/test/test_progression_kelp_forest.py
Normal file
@@ -0,0 +1,54 @@
|
||||
"""
|
||||
Author: Louis M
|
||||
Date: Fri, 03 May 2024 14:07:35 +0000
|
||||
Description: Unit test used to test that progression items can be put in Kelp Forest when option enabled
|
||||
"""
|
||||
|
||||
from . import AquariaTestBase
|
||||
from ..Locations import AquariaLocationNames
|
||||
|
||||
|
||||
class ProgressionKelpForestTest(AquariaTestBase):
|
||||
"""Unit test used to test that progression items can be put in Kelp Forest when option enabled"""
|
||||
options = {
|
||||
"no_progression_kelp_forest": False
|
||||
}
|
||||
|
||||
unfillable_locations = [
|
||||
AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_BULB_IN_THE_BOTTOM_LEFT_CLEARING,
|
||||
AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_BULB_IN_THE_PATH_DOWN_FROM_THE_TOP_LEFT_CLEARING,
|
||||
AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_BULB_IN_THE_TOP_LEFT_CLEARING,
|
||||
AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_JELLY_EGG,
|
||||
AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_BULB_CLOSE_TO_THE_VERSE_EGG,
|
||||
AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_VERSE_EGG,
|
||||
AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BULB_UNDER_THE_ROCK_IN_THE_RIGHT_PATH,
|
||||
AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BULB_AT_THE_LEFT_OF_THE_CENTER_CLEARING,
|
||||
AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BULB_IN_THE_LEFT_PATH_S_BIG_ROOM,
|
||||
AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BULB_IN_THE_LEFT_PATH_S_SMALL_ROOM,
|
||||
AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BULB_AT_THE_TOP_OF_THE_CENTER_CLEARING,
|
||||
AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BLACK_PEARL,
|
||||
AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BULB_IN_THE_TOP_FISH_PASS,
|
||||
AquariaLocationNames.KELP_FOREST_BOTTOM_LEFT_AREA_BULB_CLOSE_TO_THE_SPIRIT_CRYSTALS,
|
||||
AquariaLocationNames.KELP_FOREST_BOTTOM_LEFT_AREA_WALKER_BABY,
|
||||
AquariaLocationNames.KELP_FOREST_BOTTOM_LEFT_AREA_TRANSTURTLE,
|
||||
AquariaLocationNames.KELP_FOREST_BOTTOM_RIGHT_AREA_ODD_CONTAINER,
|
||||
AquariaLocationNames.KELP_FOREST_BOSS_AREA_BEATING_DRUNIAN_GOD,
|
||||
AquariaLocationNames.KELP_FOREST_BOSS_ROOM_BULB_AT_THE_BOTTOM_OF_THE_AREA,
|
||||
AquariaLocationNames.KELP_FOREST_BOTTOM_LEFT_AREA_FISH_CAVE_PUZZLE,
|
||||
AquariaLocationNames.KELP_FOREST_SPRITE_CAVE_BULB_INSIDE_THE_FISH_PASS,
|
||||
AquariaLocationNames.KELP_FOREST_SPRITE_CAVE_BULB_IN_THE_SECOND_ROOM,
|
||||
AquariaLocationNames.KELP_FOREST_SPRITE_CAVE_SEED_BAG,
|
||||
AquariaLocationNames.MERMOG_CAVE_BULB_IN_THE_LEFT_PART_OF_THE_CAVE,
|
||||
AquariaLocationNames.MERMOG_CAVE_PIRANHA_EGG,
|
||||
]
|
||||
|
||||
def test_progression_kelp_forest(self) -> None:
|
||||
"""
|
||||
Unit test used to test that progression items can be put in Kelp Forest when option enabled
|
||||
"""
|
||||
for location in self.unfillable_locations:
|
||||
for item_name in self.world.item_names:
|
||||
item = self.get_item_by_name(item_name)
|
||||
self.assertTrue(
|
||||
self.world.get_location(location).can_fill(self.multiworld.state, item, False),
|
||||
"The location \"" + location + "\" cannot be filled with \"" + item_name + "\"")
|
||||
31
worlds/aquaria/test/test_progression_mithalas.py
Normal file
31
worlds/aquaria/test/test_progression_mithalas.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""
|
||||
Author: Louis M
|
||||
Date: Fri, 03 May 2024 14:07:35 +0000
|
||||
Description: Unit test used to test that progression items can be put in Mithalas area when option enabled
|
||||
"""
|
||||
|
||||
from . import AquariaTestBase
|
||||
from ..Locations import AquariaLocationNames
|
||||
|
||||
|
||||
class ProgressionMithalasTest(AquariaTestBase):
|
||||
"""Unit test used to test that progression items can be put in Mithalas area when option enabled"""
|
||||
options = {
|
||||
"no_progression_mithalas": False
|
||||
}
|
||||
|
||||
unfillable_locations = [
|
||||
AquariaLocationNames.SIMON_SAYS_AREA_BEATING_SIMON_SAYS,
|
||||
AquariaLocationNames.SIMON_SAYS_AREA_TRANSTURTLE,
|
||||
]
|
||||
|
||||
def test_progression_mithalas(self) -> None:
|
||||
"""
|
||||
Unit test used to test that progression items can be put in Mithalas area when option enabled
|
||||
"""
|
||||
for location in self.unfillable_locations:
|
||||
for item_name in self.world.item_names:
|
||||
item = self.get_item_by_name(item_name)
|
||||
self.assertTrue(
|
||||
self.world.get_location(location).can_fill(self.multiworld.state, item, False),
|
||||
"The location \"" + location + "\" cannot be filled with \"" + item_name + "\"")
|
||||
31
worlds/aquaria/test/test_progression_simon_says.py
Normal file
31
worlds/aquaria/test/test_progression_simon_says.py
Normal file
@@ -0,0 +1,31 @@
|
||||
"""
|
||||
Author: Louis M
|
||||
Date: Fri, 03 May 2024 14:07:35 +0000
|
||||
Description: Unit test used to test that progression items can be put in Simon says area when option enabled
|
||||
"""
|
||||
|
||||
from . import AquariaTestBase
|
||||
from ..Locations import AquariaLocationNames
|
||||
|
||||
|
||||
class ProgressionSimonSaysTest(AquariaTestBase):
|
||||
"""Unit test used to test that progression items can be put in Simon says area when option enabled"""
|
||||
options = {
|
||||
"no_progression_simon_says": False
|
||||
}
|
||||
|
||||
unfillable_locations = [
|
||||
AquariaLocationNames.SIMON_SAYS_AREA_BEATING_SIMON_SAYS,
|
||||
AquariaLocationNames.SIMON_SAYS_AREA_TRANSTURTLE,
|
||||
]
|
||||
|
||||
def test_progression_simon_says(self) -> None:
|
||||
"""
|
||||
Unit test used to test that progression items can be put in Simon says area when option enabled
|
||||
"""
|
||||
for location in self.unfillable_locations:
|
||||
for item_name in self.world.item_names:
|
||||
item = self.get_item_by_name(item_name)
|
||||
self.assertTrue(
|
||||
self.world.get_location(location).can_fill(self.multiworld.state, item, False),
|
||||
"The location \"" + location + "\" cannot be filled with \"" + item_name + "\"")
|
||||
60
worlds/aquaria/test/test_progression_veil.py
Normal file
60
worlds/aquaria/test/test_progression_veil.py
Normal file
@@ -0,0 +1,60 @@
|
||||
"""
|
||||
Author: Louis M
|
||||
Date: Fri, 03 May 2024 14:07:35 +0000
|
||||
Description: Unit test used to test that progression items can be put in the Veil area when option enabled
|
||||
"""
|
||||
|
||||
from . import AquariaTestBase
|
||||
from ..Locations import AquariaLocationNames
|
||||
|
||||
|
||||
class ProgressionVeilTest(AquariaTestBase):
|
||||
"""Unit test used to test that progression items can be put in the Veil area when option enabled"""
|
||||
options = {
|
||||
"no_progression_veil": False
|
||||
}
|
||||
|
||||
unfillable_locations = [
|
||||
AquariaLocationNames.THE_VEIL_TOP_LEFT_AREA_IN_LI_S_CAVE,
|
||||
AquariaLocationNames.THE_VEIL_TOP_LEFT_AREA_BULB_UNDER_THE_ROCK_IN_THE_TOP_RIGHT_PATH,
|
||||
AquariaLocationNames.THE_VEIL_TOP_LEFT_AREA_BULB_HIDDEN_BEHIND_THE_BLOCKING_ROCK,
|
||||
AquariaLocationNames.THE_VEIL_TOP_LEFT_AREA_TRANSTURTLE,
|
||||
AquariaLocationNames.THE_VEIL_TOP_LEFT_AREA_BULB_INSIDE_THE_FISH_PASS,
|
||||
AquariaLocationNames.TURTLE_CAVE_TURTLE_EGG,
|
||||
AquariaLocationNames.TURTLE_CAVE_BULB_IN_BUBBLE_CLIFF,
|
||||
AquariaLocationNames.TURTLE_CAVE_URCHIN_COSTUME,
|
||||
AquariaLocationNames.THE_VEIL_TOP_RIGHT_AREA_BULB_IN_THE_MIDDLE_OF_THE_WALL_JUMP_CLIFF,
|
||||
AquariaLocationNames.THE_VEIL_TOP_RIGHT_AREA_GOLDEN_STARFISH,
|
||||
AquariaLocationNames.THE_VEIL_TOP_RIGHT_AREA_BULB_AT_THE_TOP_OF_THE_WATERFALL,
|
||||
AquariaLocationNames.THE_VEIL_TOP_RIGHT_AREA_TRANSTURTLE,
|
||||
AquariaLocationNames.THE_VEIL_BOTTOM_AREA_BULB_IN_THE_LEFT_PATH,
|
||||
AquariaLocationNames.THE_VEIL_BOTTOM_AREA_BULB_IN_THE_SPIRIT_PATH,
|
||||
AquariaLocationNames.THE_VEIL_BOTTOM_AREA_VERSE_EGG,
|
||||
AquariaLocationNames.THE_VEIL_BOTTOM_AREA_STONE_HEAD,
|
||||
AquariaLocationNames.OCTOPUS_CAVE_DUMBO_EGG,
|
||||
AquariaLocationNames.OCTOPUS_CAVE_BULB_IN_THE_PATH_BELOW_THE_OCTOPUS_CAVE_PATH,
|
||||
AquariaLocationNames.SUN_TEMPLE_BULB_IN_THE_TOP_LEFT_PART,
|
||||
AquariaLocationNames.SUN_TEMPLE_BULB_IN_THE_TOP_RIGHT_PART,
|
||||
AquariaLocationNames.SUN_TEMPLE_BULB_AT_THE_TOP_OF_THE_HIGH_DARK_ROOM,
|
||||
AquariaLocationNames.SUN_TEMPLE_GOLDEN_GEAR,
|
||||
AquariaLocationNames.SUN_TEMPLE_FIRST_BULB_OF_THE_TEMPLE,
|
||||
AquariaLocationNames.SUN_TEMPLE_BULB_ON_THE_RIGHT_PART,
|
||||
AquariaLocationNames.SUN_TEMPLE_BULB_IN_THE_HIDDEN_ROOM_OF_THE_RIGHT_PART,
|
||||
AquariaLocationNames.SUN_TEMPLE_SUN_KEY,
|
||||
AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_FIRST_PATH_BULB,
|
||||
AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_SECOND_PATH_BULB,
|
||||
AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_FIRST_CLIFF_BULB,
|
||||
AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_SECOND_CLIFF_BULB,
|
||||
AquariaLocationNames.SUN_TEMPLE_BOSS_AREA_BEATING_LUMEREAN_GOD,
|
||||
]
|
||||
|
||||
def test_progression_veil(self) -> None:
|
||||
"""
|
||||
Unit test used to test that progression items can be put in the Veil area when option enabled
|
||||
"""
|
||||
for location in self.unfillable_locations:
|
||||
for item_name in self.world.item_names:
|
||||
item = self.get_item_by_name(item_name)
|
||||
self.assertTrue(
|
||||
self.world.get_location(location).can_fill(self.multiworld.state, item, False),
|
||||
"The location \"" + location + "\" cannot be filled with \"" + item_name + "\"")
|
||||
30
worlds/aquaria/test/test_sun_form_access_four_gods.py
Normal file
30
worlds/aquaria/test/test_sun_form_access_four_gods.py
Normal file
@@ -0,0 +1,30 @@
|
||||
"""
|
||||
Author: Louis M
|
||||
Date: Thu, 18 Apr 2024 18:45:56 +0000
|
||||
Description: Unit test used to test accessibility of locations with and without the sun form with the goal four gods
|
||||
"""
|
||||
|
||||
from . import AquariaTestBase
|
||||
from ..Items import ItemNames
|
||||
from ..Locations import AquariaLocationNames
|
||||
from ..Options import UnconfineHomeWater, Objective
|
||||
|
||||
|
||||
class SunFormFourGodsAccessTest(AquariaTestBase):
|
||||
"""Unit test used to test accessibility of locations with and without the sun form with the goal four gods"""
|
||||
options = {
|
||||
"unconfine_home_water": UnconfineHomeWater.option_via_energy_door,
|
||||
"objective": Objective.option_killing_the_four_gods
|
||||
}
|
||||
|
||||
|
||||
def test_sun_form_location(self) -> None:
|
||||
"""Test locations that require sun form with the goal four gods"""
|
||||
locations = [
|
||||
AquariaLocationNames.OCTOPUS_CAVE_DUMBO_EGG,
|
||||
AquariaLocationNames.BEATING_OCTOPUS_PRIME,
|
||||
AquariaLocationNames.BEATING_THE_GOLEM,
|
||||
AquariaLocationNames.SUNKEN_CITY_CLEARED
|
||||
]
|
||||
items = [[ItemNames.SUN_FORM]]
|
||||
self.assertAccessDependency(locations, items, True)
|
||||
29
worlds/aquaria/test/test_sun_temple_cliffs.py
Normal file
29
worlds/aquaria/test/test_sun_temple_cliffs.py
Normal file
@@ -0,0 +1,29 @@
|
||||
"""
|
||||
Author: Louis M
|
||||
Date: Sun, 06 Apr 2025 14:00:32 +0000
|
||||
Description: Unit test used to test Sun Temple cliffs access
|
||||
"""
|
||||
from BaseClasses import CollectionState
|
||||
from . import AquariaTestBase
|
||||
from ..Items import ItemNames
|
||||
from ..Options import UnconfineHomeWater
|
||||
from ..Locations import AquariaLocationNames
|
||||
|
||||
|
||||
class SunTempleCliffAccessTest(AquariaTestBase):
|
||||
"""Unit test used to test Sun Temple cliffs access"""
|
||||
options = {
|
||||
"unconfine_home_water": UnconfineHomeWater.option_via_energy_door
|
||||
}
|
||||
|
||||
def test_sun_temple_cliff_access(self) -> None:
|
||||
"""test Sun Temple cliffs access"""
|
||||
state = CollectionState(self.multiworld)
|
||||
state.collect(self.get_item_by_name(ItemNames.BEAST_FORM))
|
||||
state.collect(self.get_item_by_name(ItemNames.SUN_FORM))
|
||||
first_cliff_location = self.multiworld.get_location(
|
||||
AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_FIRST_CLIFF_BULB, 1)
|
||||
self.assertFalse(first_cliff_location.can_reach(state))
|
||||
second_cliff_location = self.multiworld.get_location(
|
||||
AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_SECOND_CLIFF_BULB, 1)
|
||||
self.assertFalse(second_cliff_location.can_reach(state))
|
||||
@@ -84,7 +84,7 @@ celeste_base_id: int = 0xCA10000
|
||||
|
||||
|
||||
class CelesteItem(Item):
|
||||
game = "Celeste"
|
||||
game = "Celeste (Open World)"
|
||||
|
||||
|
||||
class CelesteItemData(NamedTuple):
|
||||
@@ -259,6 +259,7 @@ def generate_item_groups() -> dict[str, list[str]]:
|
||||
"Blue Bubbles": [ItemName.blue_boosters],
|
||||
"Red Bubbles": [ItemName.red_boosters],
|
||||
"Touch Switches": [ItemName.coins],
|
||||
"Shields": [ItemName.coins],
|
||||
}
|
||||
|
||||
return item_groups
|
||||
|
||||
@@ -16,7 +16,7 @@ celeste_base_id: int = 0xCA10000
|
||||
|
||||
|
||||
class CelesteLocation(Location):
|
||||
game = "Celeste"
|
||||
game = "Celeste (Open World)"
|
||||
|
||||
|
||||
class CelesteLocationData(NamedTuple):
|
||||
|
||||
@@ -42,7 +42,7 @@ class CelesteOpenWorld(World):
|
||||
options_dataclass = CelesteOptions
|
||||
options: CelesteOptions
|
||||
|
||||
apworld_version = 10005
|
||||
apworld_version = 10007
|
||||
|
||||
level_data: dict[str, Level] = load_logic_data()
|
||||
|
||||
|
||||
@@ -2,5 +2,5 @@
|
||||
"game": "Celeste (Open World)",
|
||||
"authors": [ "PoryGone" ],
|
||||
"minimum_ap_version": "0.6.3",
|
||||
"world_version": "1.0.5"
|
||||
"world_version": "1.0.7"
|
||||
}
|
||||
@@ -5413,13 +5413,13 @@
|
||||
"name": "north-west",
|
||||
"direction": "left",
|
||||
"blocked": false,
|
||||
"closes_behind": true
|
||||
"closes_behind": false
|
||||
},
|
||||
{
|
||||
"name": "west",
|
||||
"direction": "left",
|
||||
"blocked": false,
|
||||
"closes_behind": true
|
||||
"closes_behind": false
|
||||
},
|
||||
{
|
||||
"name": "east",
|
||||
@@ -14326,7 +14326,7 @@
|
||||
"name": "golden",
|
||||
"display_name": "Golden Strawberry",
|
||||
"type": "golden_strawberry",
|
||||
"rule": [ [ "blue_clouds", "pink_clouds", "blue_boosters", "move_blocks", "dash_refills", "springs", "coins" ] ]
|
||||
"rule": [ [ "blue_clouds", "pink_clouds", "blue_boosters", "move_blocks", "moving_platforms", "dash_refills", "springs", "coins" ] ]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -16985,7 +16985,7 @@
|
||||
"name": "strawberry",
|
||||
"display_name": "Strawberry",
|
||||
"type": "strawberry",
|
||||
"rule": [ [ "red_boosters" ] ]
|
||||
"rule": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -17577,7 +17577,7 @@
|
||||
"name": "strawberry",
|
||||
"display_name": "Strawberry",
|
||||
"type": "strawberry",
|
||||
"rule": [ [ "red_boosters" ] ]
|
||||
"rule": []
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -19953,7 +19953,7 @@
|
||||
"name": "golden",
|
||||
"display_name": "Golden Strawberry",
|
||||
"type": "golden_strawberry",
|
||||
"rule": [ [ "red_boosters", "swap_blocks", "dash_switches", "Entrance Key", "Depths Key", "Search Key 1", "Search Key 2", "seekers", "coins", "theo_crystal" ] ]
|
||||
"rule": [ [ "red_boosters", "swap_blocks", "dash_switches", "dash_refills", "Entrance Key", "Depths Key", "Search Key 1", "Search Key 2", "seekers", "coins", "theo_crystal" ] ]
|
||||
}
|
||||
]
|
||||
}
|
||||
@@ -23479,7 +23479,7 @@
|
||||
"name": "west",
|
||||
"direction": "left",
|
||||
"blocked": false,
|
||||
"closes_behind": true
|
||||
"closes_behind": false
|
||||
},
|
||||
{
|
||||
"name": "top",
|
||||
@@ -25250,7 +25250,7 @@
|
||||
"name": "west",
|
||||
"direction": "left",
|
||||
"blocked": false,
|
||||
"closes_behind": true
|
||||
"closes_behind": false
|
||||
},
|
||||
{
|
||||
"name": "east",
|
||||
@@ -37237,13 +37237,13 @@
|
||||
"name": "binoculars",
|
||||
"display_name": "Binoculars",
|
||||
"type": "binoculars",
|
||||
"rule": [ [ "breaker_boxes" ] ]
|
||||
"rule": [ [ "breaker_boxes", "double_dash_refills", "dash_switches" ] ]
|
||||
},
|
||||
{
|
||||
"name": "key_2",
|
||||
"display_name": "Power Source Key 2",
|
||||
"type": "key",
|
||||
"rule": [ [ "breaker_boxes", "double_dash_refills", "jellyfish" ] ]
|
||||
"rule": [ [ "breaker_boxes", "double_dash_refills", "dash_switches", "jellyfish" ] ]
|
||||
}
|
||||
]
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -18,8 +18,8 @@ if __name__ == "__main__":
|
||||
for level in level_data["levels"]:
|
||||
level_str = (f" \"{level['name']}\": Level(\"{level['name']}\", "
|
||||
f"\"{level['display_name']}\", "
|
||||
f"[room for _, room in all_rooms.items() if room.level_name == \"{level['name']}\"], "
|
||||
f"[room_con for _, room_con in all_room_connections.items() if room_con.level_name == \"{level['name']}\"]),"
|
||||
f"rooms_by_level[\"{level['name']}\"], "
|
||||
f"room_cons_by_level[\"{level['name']}\"]),"
|
||||
)
|
||||
|
||||
all_levels.append(level_str)
|
||||
@@ -31,8 +31,8 @@ if __name__ == "__main__":
|
||||
|
||||
room_str = (f" \"{room_full_name}\": Room(\"{level['name']}\", "
|
||||
f"\"{room_full_name}\", \"{room_full_display_name}\", "
|
||||
f"[reg for _, reg in all_regions.items() if reg.room_name == \"{room_full_name}\"], "
|
||||
f"[door for _, door in all_doors.items() if door.room_name == \"{room_full_name}\"]"
|
||||
f"regions_by_room[\"{room_full_name}\"], "
|
||||
f"doors_by_room[\"{room_full_name}\"]"
|
||||
)
|
||||
|
||||
if "checkpoint" in room and room["checkpoint"] != "":
|
||||
@@ -47,8 +47,8 @@ if __name__ == "__main__":
|
||||
|
||||
region_str = (f" \"{region_full_name}\": PreRegion(\"{region_full_name}\", "
|
||||
f"\"{room_full_name}\", "
|
||||
f"[reg_con for _, reg_con in all_region_connections.items() if reg_con.source_name == \"{region_full_name}\"], "
|
||||
f"[loc for _, loc in all_locations.items() if loc.region_name == \"{region_full_name}\"]),"
|
||||
f"connections_by_region[\"{region_full_name}\"], "
|
||||
f"locations_by_region[\"{region_full_name}\"]),"
|
||||
)
|
||||
|
||||
all_regions.append(region_str)
|
||||
@@ -150,6 +150,7 @@ if __name__ == "__main__":
|
||||
print("")
|
||||
print("from ..Levels import Level, Room, PreRegion, LevelLocation, RegionConnection, RoomConnection, Door, DoorDirection, LocationType")
|
||||
print("from ..Names import ItemName")
|
||||
print(f"from collections import defaultdict")
|
||||
print("")
|
||||
print("all_doors: dict[str, Door] = {")
|
||||
for line in all_doors:
|
||||
@@ -166,6 +167,15 @@ if __name__ == "__main__":
|
||||
print(line)
|
||||
print("}")
|
||||
print("")
|
||||
print("connections_by_region: defaultdict[str, list[RegionConnection]] = defaultdict(lambda: [])")
|
||||
print("locations_by_region: defaultdict[str, list[LevelLocation]] = defaultdict(lambda: [])")
|
||||
print("")
|
||||
print("for _, connection in all_region_connections.items():")
|
||||
print(" connections_by_region[connection.source_name].append(connection)")
|
||||
print("")
|
||||
print("for _, location in all_locations.items():")
|
||||
print(" locations_by_region[location.region_name].append(location)")
|
||||
print("")
|
||||
print("all_regions: dict[str, PreRegion] = {")
|
||||
for line in all_regions:
|
||||
print(line)
|
||||
@@ -176,11 +186,29 @@ if __name__ == "__main__":
|
||||
print(line)
|
||||
print("}")
|
||||
print("")
|
||||
print("regions_by_room: defaultdict[str, list[PreRegion]] = defaultdict(lambda: [])")
|
||||
print("doors_by_room: defaultdict[str, list[Door]] = defaultdict(lambda: [])")
|
||||
print("")
|
||||
print("for _, region in all_regions.items():")
|
||||
print(" regions_by_room[region.room_name].append(region)")
|
||||
print("")
|
||||
print("for _, door in all_doors.items():")
|
||||
print(" doors_by_room[door.room_name].append(door)")
|
||||
print("")
|
||||
print("all_rooms: dict[str, Room] = {")
|
||||
for line in all_rooms:
|
||||
print(line)
|
||||
print("}")
|
||||
print("")
|
||||
print("rooms_by_level: defaultdict[str, list[Room]] = defaultdict(lambda: [])")
|
||||
print("room_cons_by_level: defaultdict[str, list[RoomConnection]] = defaultdict(lambda: [])")
|
||||
print("")
|
||||
print("for _, room in all_rooms.items():")
|
||||
print(" rooms_by_level[room.level_name].append(room)")
|
||||
print("")
|
||||
print("for _, room_con in all_room_connections.items():")
|
||||
print(" room_cons_by_level[room_con.level_name].append(room_con)")
|
||||
print("")
|
||||
print("all_levels: dict[str, Level] = {")
|
||||
for line in all_levels:
|
||||
print(line)
|
||||
|
||||
@@ -218,10 +218,11 @@ class RandomizeEnemiesOption(DefaultOnToggle):
|
||||
|
||||
|
||||
class SimpleEarlyBossesOption(DefaultOnToggle):
|
||||
"""Avoid replacing Iudex Gundyr and Vordt with late bosses.
|
||||
"""Avoid replacing Iudex Gundyr and Vordt with difficult bosses.
|
||||
|
||||
This excludes all bosses after Dancer of the Boreal Valley from these two boss fights. Disable
|
||||
it for a chance at a much harder early game.
|
||||
This limits these fights to bosses that are known to scale gracefully for low-level fights.
|
||||
This doesn't necessarily mean that those bosses will be from the early game, just that they're
|
||||
not too difficult when scaled down.
|
||||
|
||||
This is ignored unless enemies are randomized.
|
||||
"""
|
||||
|
||||
516
worlds/earthbound/Client.py
Normal file
516
worlds/earthbound/Client.py
Normal file
@@ -0,0 +1,516 @@
|
||||
import logging
|
||||
import struct
|
||||
import typing
|
||||
import time
|
||||
import uuid
|
||||
from struct import pack
|
||||
from .game_data.local_data import client_specials, world_version, hint_bits, item_id_table, money_id_table
|
||||
from .game_data.text_data import text_encoder
|
||||
from .gifting.gift_tags import gift_properties
|
||||
from .gifting.trait_parser import wanted_traits, trait_interpreter, gift_exclusions
|
||||
|
||||
from NetUtils import ClientStatus, color
|
||||
from worlds.AutoSNIClient import SNIClient
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from SNIClient import SNIContext
|
||||
else:
|
||||
SNIContext = typing.Any
|
||||
|
||||
snes_logger = logging.getLogger("SNES")
|
||||
|
||||
ROM_START = 0x000000
|
||||
WRAM_START = 0xF50000
|
||||
WRAM_SIZE = 0x20000
|
||||
SRAM_START = 0xE00000
|
||||
|
||||
EB_ROMHASH_START = 0x00FFC0
|
||||
WORLD_VERSION = 0x3FF0A0
|
||||
ROMHASH_SIZE = 0x15
|
||||
|
||||
ITEM_MODE = ROM_START + 0x04FD76
|
||||
|
||||
ITEMQUEUE_HIGH = WRAM_START + 0xB576
|
||||
ITEM_RECEIVED = WRAM_START + 0xB570
|
||||
SPECIAL_RECEIVED = WRAM_START + 0xB572
|
||||
MONEY_RECIVED = WRAM_START + 0xB5F1
|
||||
SAVE_FILE = WRAM_START + 0xB4A1
|
||||
GIYGAS_CLEAR = WRAM_START + 0x9C11
|
||||
GAME_CLEAR = WRAM_START + 0x9C85
|
||||
OPEN_WINDOW = WRAM_START + 0x8958
|
||||
MELODY_TABLE = WRAM_START + 0x9C1E
|
||||
EARTH_POWER_FLAG = WRAM_START + 0x9C82
|
||||
CUR_SCENE = WRAM_START + 0x97B8
|
||||
IS_IN_BATTLE = WRAM_START + 0x9643
|
||||
DEATHLINK_ENABLED = ROM_START + 0x04FD74
|
||||
DEATHLINK_TYPE = ROM_START + 0x04FD75
|
||||
IS_CURRENTLY_DEAD = WRAM_START + 0xB582
|
||||
GOT_DEATH_FROM_SERVER = WRAM_START + 0xB583
|
||||
PLAYER_JUST_DIED_SEND_DEATHLINK = WRAM_START + 0xB584
|
||||
IS_ABLE_TO_RECEIVE_DEATHLINKS = WRAM_START + 0xB585
|
||||
CHAR_COUNT = WRAM_START + 0x98A4
|
||||
OSS_FLAG = WRAM_START + 0x5D98
|
||||
HINT_SCOUNT_IDS = ROM_START + 0x310250
|
||||
SCOUTED_HINT_FLAGS = WRAM_START + 0xB621
|
||||
MONEY_IN_BANK = WRAM_START + 0x9835
|
||||
IS_ENERGYLINK_ENABLED = ROM_START + 0x04FD78
|
||||
already_tried_to_connect = False
|
||||
|
||||
|
||||
class EarthBoundClient(SNIClient):
|
||||
game = "EarthBound"
|
||||
patch_suffix = ".apeb"
|
||||
most_recent_connect: str = ""
|
||||
client_version: str = world_version
|
||||
hint_list: list[int] = []
|
||||
hinted_shop_locations: list[int] = []
|
||||
|
||||
async def deathlink_kill_player(self, ctx: "SNIContext") -> None:
|
||||
from SNIClient import DeathState, snes_buffered_write, snes_flush_writes, snes_read
|
||||
battle_hp = {
|
||||
1: WRAM_START + 0x9FBF,
|
||||
2: WRAM_START + 0xA00D,
|
||||
3: WRAM_START + 0xA05B,
|
||||
4: WRAM_START + 0xA0A9,
|
||||
}
|
||||
|
||||
active_hp = {
|
||||
1: WRAM_START + 0x9A15,
|
||||
2: WRAM_START + 0x9A74,
|
||||
3: WRAM_START + 0x9AD3,
|
||||
4: WRAM_START + 0x9B32,
|
||||
}
|
||||
|
||||
scrolling_hp = {
|
||||
1: WRAM_START + 0x9A13,
|
||||
2: WRAM_START + 0x9A72,
|
||||
3: WRAM_START + 0x9AD1,
|
||||
4: WRAM_START + 0x9B30,
|
||||
}
|
||||
|
||||
deathlink_mode = await snes_read(ctx, DEATHLINK_TYPE, 1)
|
||||
oss_flag = await snes_read(ctx, OSS_FLAG, 1)
|
||||
is_currently_dead = await snes_read(ctx, IS_CURRENTLY_DEAD, 1)
|
||||
can_receive_deathlinks = await snes_read(ctx, IS_ABLE_TO_RECEIVE_DEATHLINKS, 1)
|
||||
is_in_battle = await snes_read(ctx, IS_IN_BATTLE, 1)
|
||||
char_count = await snes_read(ctx, CHAR_COUNT, 1)
|
||||
snes_buffered_write(ctx, GOT_DEATH_FROM_SERVER, bytes([0x01]))
|
||||
text_open = await snes_read(ctx, OPEN_WINDOW, 1)
|
||||
|
||||
if text_open is None: #Catch None reads from client jank????????
|
||||
return
|
||||
|
||||
if is_currently_dead[0] != 0x00 or can_receive_deathlinks[0] == 0x00:
|
||||
return
|
||||
|
||||
# If suppression is set and we're not in a battle dont do deathlinks
|
||||
if oss_flag[0] != 0x00 and is_in_battle[0] == 0x00:
|
||||
return
|
||||
|
||||
# Prevent overworld deaths while a menu is open
|
||||
if not is_in_battle[0] and text_open[0] != 0xFF:
|
||||
return
|
||||
|
||||
for i in range(char_count[0]):
|
||||
w_cur_char = WRAM_START + 0x986F + i
|
||||
current_char = await snes_read(ctx, w_cur_char, 1)
|
||||
snes_buffered_write(ctx, active_hp[current_char[0]], bytes([0x00, 0x00]))
|
||||
snes_buffered_write(ctx, battle_hp[i + 1], bytes([0x00, 0x00]))
|
||||
if deathlink_mode[0] == 0 or is_in_battle[0] == 0:
|
||||
# This should be the check for instant or mercy. Write the value, call it here
|
||||
snes_buffered_write(ctx, scrolling_hp[current_char[0]], bytes([0x00, 0x00]))
|
||||
await snes_flush_writes(ctx)
|
||||
ctx.death_state = DeathState.dead
|
||||
ctx.last_death_link = time.time()
|
||||
|
||||
def on_package(self, ctx, cmd: str, args: dict[str, typing.Any]) -> None:
|
||||
super().on_package(ctx, cmd, args)
|
||||
|
||||
if cmd == "Connected":
|
||||
self.slot_data = args.get("slot_data", None)
|
||||
|
||||
async def validate_rom(self, ctx: "SNIContext") -> bool:
|
||||
from SNIClient import snes_read
|
||||
|
||||
rom_name = await snes_read(ctx, EB_ROMHASH_START, ROMHASH_SIZE)
|
||||
apworld_version = await snes_read(ctx, WORLD_VERSION, 16)
|
||||
|
||||
item_handling = await snes_read(ctx, ITEM_MODE, 1)
|
||||
if rom_name is None or rom_name[:6] != b"MOM2AP":
|
||||
return False
|
||||
|
||||
apworld_version = apworld_version.decode("utf-8").strip("\x00")
|
||||
if apworld_version != self.most_recent_connect and apworld_version != self.client_version:
|
||||
ctx.gui_error("Bad Version", f"EarthBound APWorld version {self.client_version} does not match generated version {apworld_version}")
|
||||
self.most_recent_connect = apworld_version
|
||||
return False
|
||||
|
||||
ctx.game = self.game
|
||||
if item_handling[0] == 0x00:
|
||||
ctx.items_handling = 0b001
|
||||
else:
|
||||
ctx.items_handling = 0b111
|
||||
ctx.rom = rom_name
|
||||
|
||||
death_link = await snes_read(ctx, DEATHLINK_ENABLED, 1)
|
||||
if death_link:
|
||||
await ctx.update_death_link(bool(death_link[0] & 0b1))
|
||||
return True
|
||||
|
||||
async def game_watcher(self, ctx: "SNIContext") -> None:
|
||||
from SNIClient import snes_buffered_write, snes_flush_writes, snes_read, snes_write
|
||||
giygas_clear = await snes_read(ctx, GIYGAS_CLEAR, 0x1)
|
||||
game_clear = await snes_read(ctx, GAME_CLEAR, 0x1)
|
||||
item_received = await snes_read(ctx, ITEM_RECEIVED, 0x1)
|
||||
special_received = await snes_read(ctx, SPECIAL_RECEIVED, 0x1)
|
||||
money_received = await snes_read(ctx, MONEY_RECIVED, 0x2)
|
||||
save_num = await snes_read(ctx, SAVE_FILE, 0x1)
|
||||
text_open = await snes_read(ctx, OPEN_WINDOW, 1)
|
||||
melody_table = await snes_read(ctx, MELODY_TABLE, 2)
|
||||
earth_power_absorbed = await snes_read(ctx, EARTH_POWER_FLAG, 1)
|
||||
cur_script = await snes_read(ctx, CUR_SCENE, 1)
|
||||
rom = await snes_read(ctx, EB_ROMHASH_START, ROMHASH_SIZE)
|
||||
scouted_hint_flags = await snes_read(ctx, SCOUTED_HINT_FLAGS, 1)
|
||||
gift_target = await snes_read(ctx, WRAM_START + 0xB5E7, 2)
|
||||
outbound_gifts = await snes_read(ctx, WRAM_START + 0x31D0, 1)
|
||||
shop_scout = await snes_read(ctx, WRAM_START + 0x0770, 1)
|
||||
shop_scouts_enabled = await snes_read(ctx, ROM_START + 0x04FD77, 1)
|
||||
outgoing_energy = await snes_read(ctx, MONEY_IN_BANK, 4)
|
||||
if rom != ctx.rom:
|
||||
ctx.rom = None
|
||||
return
|
||||
|
||||
if giygas_clear[0] & 0x01 == 0x01: # Are we in the epilogue
|
||||
return
|
||||
|
||||
if save_num[0] == 0x00: # If on the title screen
|
||||
return
|
||||
|
||||
if ctx.slot is None:
|
||||
return
|
||||
|
||||
if outgoing_energy is None: #None Catcher
|
||||
return
|
||||
|
||||
|
||||
if f"GiftBoxes;{ctx.team}" not in ctx.stored_data:
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "SetNotify",
|
||||
"keys": [f"GiftBoxes;{ctx.team}"]
|
||||
}])
|
||||
|
||||
# GIFTING DATA
|
||||
if f"GiftBox;{ctx.team};{ctx.slot}" not in ctx.stored_data:
|
||||
local_giftbox = {
|
||||
str(ctx.slot): {
|
||||
"is_open": True,
|
||||
"accepts_any_gift": False,
|
||||
"desired_traits": wanted_traits,
|
||||
"minimum_gift_data_version": 2,
|
||||
"maximum_gift_data_version": 3}}
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "Set",
|
||||
"key": f"GiftBoxes;{ctx.team}",
|
||||
"want_reply": False,
|
||||
"default": {},
|
||||
"operations": [{"operation": "update", "value": local_giftbox}]
|
||||
}])
|
||||
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "Get",
|
||||
"keys": [f"GiftBox;{ctx.team};{ctx.slot}"]
|
||||
}])
|
||||
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "SetNotify",
|
||||
"keys": [f"GiftBox;{ctx.team};{ctx.slot}", f"GiftBoxes;{ctx.team}"]
|
||||
}])
|
||||
|
||||
inbox = ctx.stored_data.get(f"GiftBox;{ctx.team};{ctx.slot}")
|
||||
motherbox = ctx.stored_data.get(f"GiftBoxes;{ctx.team}")
|
||||
if inbox:
|
||||
gift_item_name = "None"
|
||||
key, gift = next(iter(inbox.items()))
|
||||
if "item_name" in gift or "ItemName" in gift:
|
||||
gift_item_name = gift.get("item_name", gift.get("ItemName"))
|
||||
if gift_item_name in item_id_table and gift_item_name not in gift_exclusions:
|
||||
# If the name matches an EB item, convert it to one (even if not coming from EB)
|
||||
item = item_id_table[gift_item_name]
|
||||
else:
|
||||
item = trait_interpreter(gift)
|
||||
|
||||
inbox_queue = await snes_read(ctx, WRAM_START + 0x3200, 1)
|
||||
# Pause if the receiver queue is full
|
||||
if not inbox_queue[0]:
|
||||
await snes_write(ctx, [(WRAM_START + 0x3200, bytes([item]))])
|
||||
inbox.pop(key)
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "Set",
|
||||
"key": f"GiftBox;{ctx.team};{ctx.slot}",
|
||||
"want_reply": False,
|
||||
"default": {},
|
||||
"operations": [{"operation": "pop", "value": key}]
|
||||
}])
|
||||
|
||||
# We're in the Gift selection menu. This should write the selected player's name into RAM
|
||||
# for parsing.
|
||||
# TODO; CHECK A SETNOTIFY HERE
|
||||
gift_target = int.from_bytes(gift_target, byteorder="little")
|
||||
|
||||
# Giftbox checking for the gift menu UI
|
||||
if gift_target != 0x00 and motherbox is not None:
|
||||
gift_recipient = str(gift_target)
|
||||
recip_name = ctx.player_names[gift_target]
|
||||
recip_name = get_alias(recip_name, ctx.slot_info[gift_target].name)
|
||||
recip_name = text_encoder(recip_name, 20)
|
||||
if gift_recipient in motherbox:
|
||||
if "IsOpen" in motherbox[gift_recipient]:
|
||||
motherbox[gift_recipient]["is_open"] = motherbox[gift_recipient].pop("IsOpen")
|
||||
|
||||
if gift_recipient in motherbox and motherbox[gift_recipient]["is_open"]:
|
||||
recip_name.extend(text_encoder(" (Open)", 20))
|
||||
else:
|
||||
recip_name.extend(text_encoder(" (Closed)", 20))
|
||||
recip_name.append(0x00)
|
||||
await snes_write(ctx, [(WRAM_START + 0xFF80, recip_name)])
|
||||
await snes_write(ctx, [(WRAM_START + 0xB5E7, bytes([0x00, 0x00]))])
|
||||
await snes_write(ctx, [(WRAM_START + 0xB573, bytes([0x00, 0x00]))])
|
||||
|
||||
gift_flag_byte = await snes_read(ctx, WRAM_START + 0xB622, 1)
|
||||
gift_flag_byte = gift_flag_byte[0] | 0x04
|
||||
await snes_write(ctx, [(WRAM_START + 0xB622, bytes([gift_flag_byte]))])
|
||||
|
||||
if outbound_gifts[0] != 0x00 and motherbox is not None:
|
||||
gift_buffer = await snes_read(ctx, WRAM_START + 0x31D1, 3)
|
||||
gift_item_id = gift_buffer[0]
|
||||
gift = gift_properties[gift_item_id]
|
||||
recipient = struct.unpack("H", gift_buffer[-2:])
|
||||
if str(recipient[0]) in motherbox:
|
||||
# Check if the player's box is open, refund if not
|
||||
if "IsOpen" in motherbox[str(recipient[0])]:
|
||||
# Does the recipient 0 thing work if > 255? Will need some testing.
|
||||
motherbox[str(recipient[0])]["is_open"] = motherbox[str(recipient[0])].pop("IsOpen")
|
||||
|
||||
if "AcceptsAnyGift" in motherbox[str(recipient[0])]:
|
||||
motherbox[str(recipient[0])]["accepts_any_gift"] = motherbox[str(recipient[0])].pop("AcceptsAnyGift")
|
||||
|
||||
if "DesiredTraits" in motherbox[str(recipient[0])]:
|
||||
motherbox[str(recipient[0])]["desired_traits"] = motherbox[str(recipient[0])].pop("DesiredTraits")
|
||||
|
||||
if "Trait" in motherbox[str(recipient[0])]["desired_traits"]:
|
||||
motherbox[str(recipient[0])]["desired_traits"]["trait"] = motherbox[str(recipient[0])]["desired_traits"].pop("Trait")
|
||||
|
||||
if str(recipient[0]) in motherbox and motherbox[str(recipient[0])]["is_open"] and (any(
|
||||
motherbox[str(recipient[0])]["accepts_any_gift"] or
|
||||
trait["trait"] in motherbox[str(recipient[0])]["desired_traits"] for trait in gift.traits)):
|
||||
was_refunded = False
|
||||
recipient = recipient[0]
|
||||
else:
|
||||
was_refunded = True
|
||||
recipient = ctx.slot
|
||||
guid = str(uuid.uuid4())
|
||||
outgoing_gift = {
|
||||
guid: {
|
||||
"id": guid,
|
||||
"item_name": gift.name,
|
||||
"amount": 1,
|
||||
"item_value": gift.value,
|
||||
"traits": gift.traits,
|
||||
"sender_slot": ctx.slot,
|
||||
"receiver_slot": recipient,
|
||||
"sender_team": ctx.team,
|
||||
"receiver_team": ctx.team, # ??? Should be Receive slot team?
|
||||
"is_refund": was_refunded}}
|
||||
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "Set",
|
||||
"key": f"GiftBox;{ctx.team};{recipient}", # Receiver team here too
|
||||
"want_reply": True,
|
||||
"default": {},
|
||||
"operations": [{"operation": "update", "value": outgoing_gift}]
|
||||
}])
|
||||
|
||||
gift_queue = await snes_read(ctx, WRAM_START + 0x31D4, 0x21)
|
||||
# shuffle the entire queue down 3 bytes
|
||||
outbox_full_byte = await snes_read(ctx, WRAM_START + 0xB622, 1)
|
||||
|
||||
await snes_write(ctx, [(WRAM_START + 0x31D1, gift_queue)])
|
||||
await snes_write(ctx, [(WRAM_START + 0x31D0, bytes([outbound_gifts[0] - 1]))])
|
||||
outbox_full_byte = outbox_full_byte[0] & ~0x08
|
||||
await snes_write(ctx, [(WRAM_START + 0xB622, bytes([outbox_full_byte]))])
|
||||
|
||||
if (game_clear[0] & 0x01 == 0x01) and not ctx.finished_game: # Goal should ignore the item queue and textbox check
|
||||
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
||||
ctx.finished_game = True
|
||||
|
||||
for i in range(6):
|
||||
if scouted_hint_flags[0] & hint_bits[i]:
|
||||
if i not in self.hint_list:
|
||||
scoutable_hint = await snes_read(ctx, HINT_SCOUNT_IDS + (i * 3), 3)
|
||||
if not scoutable_hint[2]:
|
||||
scoutable_hint = (int.from_bytes(scoutable_hint[:2], byteorder="little") + 0xEB0000)
|
||||
self.hint_list.append(i)
|
||||
await ctx.send_msgs([{"cmd": "CreateHints", "locations": [scoutable_hint], "player": ctx.player}])
|
||||
else:
|
||||
hint = self.slot_data['hint_man_hints'][i]
|
||||
await ctx.send_msgs([{"cmd": "CreateHints", "locations": [hint[0]], "player": hint[1]}])
|
||||
self.hint_list.append(i)
|
||||
|
||||
if shop_scout[0] and shop_scouts_enabled[0]:
|
||||
shop_slots = []
|
||||
for i in range(7):
|
||||
slot_id = (0xEB0FF9 + (shop_scout[0] * 7) + i)
|
||||
if slot_id in ctx.server_locations and slot_id not in self.hinted_shop_locations:
|
||||
shop_slots.append(slot_id)
|
||||
|
||||
if shop_slots:
|
||||
if shop_scouts_enabled[0] == 2:
|
||||
await ctx.send_msgs([{"cmd": "CreateHints", "locations": shop_slots, "player": ctx.slot}])
|
||||
await snes_write(ctx, [(WRAM_START + 0x0770, bytes([0x00]))])
|
||||
else:
|
||||
prog_shops = []
|
||||
await ctx.send_msgs([{"cmd": "LocationScouts", "locations": shop_slots, "create_as_hint": 0}])
|
||||
for location in shop_slots:
|
||||
if location in ctx.locations_info:
|
||||
self.hinted_shop_locations.append(location)
|
||||
if ctx.locations_info[location].flags & 0x01:
|
||||
prog_shops.append(location)
|
||||
if prog_shops:
|
||||
await ctx.send_msgs([{"cmd": "CreateHints", "locations": prog_shops, "player": ctx.slot}])
|
||||
|
||||
melody_data = f"{ctx.team}_{ctx.slot}_melody_status"
|
||||
earth_power_data = f"{ctx.team}_{ctx.slot}_earthpower"
|
||||
current_melodies = int.from_bytes(melody_table, "little")
|
||||
earth_power_state = int.from_bytes(earth_power_absorbed, "little")
|
||||
|
||||
if melody_data not in ctx.stored_data or (ctx.stored_data[melody_data] != current_melodies) or (ctx.stored_data[earth_power_data] != earth_power_state):
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "Set",
|
||||
"key": melody_data,
|
||||
"default": None,
|
||||
"want_reply": True,
|
||||
"operations": [{"operation": "replace", "value": int.from_bytes(melody_table, "little")}]},
|
||||
{
|
||||
"cmd": "Set",
|
||||
"key": earth_power_data,
|
||||
"default": None,
|
||||
"want_reply": True,
|
||||
"operations": [{"operation": "replace", "value": int.from_bytes(earth_power_absorbed, "little")}]
|
||||
}])
|
||||
|
||||
# death link handling goes here
|
||||
if "DeathLink" in ctx.tags and ctx.last_death_link + 1 < time.time():
|
||||
send_deathlink = await snes_read(ctx, PLAYER_JUST_DIED_SEND_DEATHLINK, 1)
|
||||
currently_dead = send_deathlink[0] != 0x00
|
||||
if send_deathlink[0] != 0x00:
|
||||
snes_buffered_write(ctx, PLAYER_JUST_DIED_SEND_DEATHLINK, bytes([0x00]))
|
||||
await ctx.handle_deathlink_state(currently_dead)
|
||||
|
||||
new_checks = []
|
||||
from .game_data.local_data import check_table
|
||||
|
||||
location_ram_data = await snes_read(ctx, WRAM_START + 0x9C00, 0x88)
|
||||
shop_location_flags = await snes_read(ctx, WRAM_START + 0xB721, 0x41)
|
||||
for loc_id, loc_data in check_table.items():
|
||||
if loc_id not in ctx.locations_checked:
|
||||
if loc_id >= 0xEB1000:
|
||||
data = shop_location_flags[loc_data[0]]
|
||||
else:
|
||||
data = location_ram_data[loc_data[0]]
|
||||
masked_data = data & (1 << loc_data[1])
|
||||
bit_set = masked_data != 0
|
||||
invert_bit = ((len(loc_data) >= 3) and loc_data[2])
|
||||
if bit_set != invert_bit and loc_id in ctx.server_locations:
|
||||
if text_open[0] == 0xFF or shop_scout[0]: # Don't check locations while in a textbox
|
||||
new_checks.append(loc_id)
|
||||
|
||||
for new_check_id in new_checks:
|
||||
ctx.locations_checked.add(new_check_id)
|
||||
location = ctx.location_names.lookup_in_slot(new_check_id)
|
||||
snes_logger.info(
|
||||
f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})')
|
||||
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [new_check_id]}])
|
||||
await snes_write(ctx, [(WRAM_START + 0x0770, bytes([0]))])
|
||||
|
||||
if item_received[0] or special_received[0] != 0x00 or money_received[0] != 0x00: # If processing any item from the server
|
||||
return
|
||||
|
||||
is_energylink_enabled = await snes_read(ctx, IS_ENERGYLINK_ENABLED, 1)
|
||||
is_requesting_energy = await snes_read(ctx, WRAM_START + 0x0790, 1)
|
||||
energy_withdrawal = await snes_read(ctx, WRAM_START + 0x0796, 4)
|
||||
ctx.set_notify(f"EnergyLink{ctx.team}")
|
||||
energy = ctx.stored_data.get(f"EnergyLink{ctx.team}", 0)
|
||||
exchange_rate = 1000000
|
||||
if is_energylink_enabled[0]:
|
||||
|
||||
deposited_energy = int.from_bytes(outgoing_energy, byteorder="little")
|
||||
if deposited_energy:
|
||||
deposited_energy *= exchange_rate
|
||||
await snes_write(ctx, [(MONEY_IN_BANK, (0x00).to_bytes(4, byteorder="little"))])
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "Set", "key": f"EnergyLink{ctx.team}", "slot": ctx.slot, "operations":
|
||||
[{"operation": "add", "value": deposited_energy},
|
||||
{"operation": "max", "value": 0}]}])
|
||||
|
||||
if is_requesting_energy[0] and energy: # This is just to pull the current number for a display.
|
||||
energy //= exchange_rate
|
||||
if energy > 9999999:
|
||||
energy = 9999999
|
||||
cap_flag = await snes_read(ctx, WRAM_START + 0xB623, 1)
|
||||
cap_flag = int.from_bytes(cap_flag)
|
||||
cap_flag |= 0x20
|
||||
await snes_write(ctx, [(WRAM_START + 0xB623, cap_flag.to_bytes(1, byteorder="little"))])
|
||||
|
||||
await snes_write(ctx, [(WRAM_START + 0x0792, int(energy).to_bytes(4, byteorder="little"))])
|
||||
await snes_write(ctx, [(WRAM_START + 0x0790, (0x00).to_bytes(1, byteorder="little"))])
|
||||
|
||||
if any(energy_withdrawal) and energy:
|
||||
withdrawal = int.from_bytes(energy_withdrawal, byteorder="little")
|
||||
withdrawal *= exchange_rate
|
||||
energy = ctx.stored_data.get(f"EnergyLink{ctx.team}", 0) # Refresh the value
|
||||
|
||||
if withdrawal > energy:
|
||||
energy_success = 2
|
||||
withdrawal = energy
|
||||
else:
|
||||
energy_success = 1
|
||||
|
||||
await snes_write(ctx, [(WRAM_START + 0x97D0, (withdrawal // exchange_rate).to_bytes(4, byteorder="little"))])
|
||||
await snes_write(ctx, [(WRAM_START + 0x0796, (0x00).to_bytes(4, byteorder="little"))])
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "Set", "key": f"EnergyLink{ctx.team}", "slot": ctx.slot, "operations":
|
||||
[{"operation": "add", "value": (withdrawal * -1)},
|
||||
{"operation": "max", "value": 0}]}])
|
||||
await snes_write(ctx, [(WRAM_START + 0x079A, energy_success.to_bytes(1, byteorder="little"))]) # Signal the game to continue
|
||||
|
||||
if cur_script[0]: # Stop items during cutscenes
|
||||
return
|
||||
|
||||
recv_count = await snes_read(ctx, ITEMQUEUE_HIGH, 2)
|
||||
recv_index = struct.unpack("H", recv_count)[0]
|
||||
if recv_index < len(ctx.items_received):
|
||||
item = ctx.items_received[recv_index]
|
||||
item_id = (item.item - 0xEB0000)
|
||||
recv_index += 1
|
||||
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
|
||||
color(ctx.item_names.lookup_in_slot(item.item), "red", "bold"),
|
||||
color(ctx.player_names[item.player], 'yellow'),
|
||||
ctx.location_names.lookup_in_slot(item.location, item.player), recv_index, len(ctx.items_received)))
|
||||
|
||||
snes_buffered_write(ctx, ITEMQUEUE_HIGH, pack("H", recv_index))
|
||||
if item_id <= 0xFD:
|
||||
snes_buffered_write(ctx, WRAM_START + 0xB570, bytes([item_id]))
|
||||
elif item_id in money_id_table:
|
||||
snes_buffered_write(ctx, WRAM_START + 0xB5F1, bytes([list(money_id_table).index(item_id) + 1]))
|
||||
else:
|
||||
snes_buffered_write(ctx, WRAM_START + 0xB572, bytes([client_specials[item_id]]))
|
||||
|
||||
await snes_flush_writes(ctx)
|
||||
|
||||
|
||||
def get_alias(alias: str, slot_name: str) -> str:
|
||||
try:
|
||||
index = alias.index(f" ({slot_name}")
|
||||
except ValueError:
|
||||
return alias
|
||||
return alias[:index]
|
||||
337
worlds/earthbound/Items.py
Normal file
337
worlds/earthbound/Items.py
Normal file
@@ -0,0 +1,337 @@
|
||||
from typing import Dict, Set, NamedTuple, Optional
|
||||
from BaseClasses import ItemClassification
|
||||
|
||||
|
||||
class ItemData(NamedTuple):
|
||||
category: str
|
||||
code: Optional[int]
|
||||
classification: ItemClassification
|
||||
amount: int = 1
|
||||
|
||||
|
||||
item_table: Dict[str, ItemData] = {
|
||||
"Franklin Badge": ItemData("Key Items", 0xEB0001, ItemClassification.progression),
|
||||
"Teddy Bear": ItemData("Characters", 0xEB0002, ItemClassification.filler, 0),
|
||||
"Super Plush Bear": ItemData("Characters", 0xEB0003, ItemClassification.useful, 0),
|
||||
|
||||
"Broken Machine": ItemData("Broken Items", 0xEB0004, ItemClassification.useful, 0),
|
||||
"Broken Gadget": ItemData("Jeff Weapons", 0xEB0005, ItemClassification.useful, 0),
|
||||
"Broken Air Gun": ItemData("Jeff Weapons", 0xEB0006, ItemClassification.filler, 0),
|
||||
"Broken Spray Can": ItemData("Broken Items", 0xEB0007, ItemClassification.filler, 0),
|
||||
"Broken Laser": ItemData("Jeff Weapons", 0xEB0008, ItemClassification.useful, 0),
|
||||
"Broken Iron": ItemData("Broken Items", 0xEB0009, ItemClassification.filler, 0),
|
||||
"Broken Pipe": ItemData("Broken Items", 0xEB000A, ItemClassification.useful, 0),
|
||||
"Broken Cannon": ItemData("Jeff Weapons", 0xEB000B, ItemClassification.useful, 0),
|
||||
"Broken Tube": ItemData("Broken Items", 0xEB000C, ItemClassification.useful, 0),
|
||||
"Broken Bazooka": ItemData("Broken Items", 0xEB000D, ItemClassification.useful, 0),
|
||||
"Broken Trumpet": ItemData("Broken Items", 0xEB000E, ItemClassification.filler, 0),
|
||||
"Broken Harmonica": ItemData("Jeff Weapons", 0xEB000F, ItemClassification.useful, 0),
|
||||
"Broken Antenna": ItemData("Jeff Weapons", 0xEB0010, ItemClassification.useful, 0),
|
||||
|
||||
"Cracked Bat": ItemData("Ness Weapons", 0xEB0011, ItemClassification.filler, 0),
|
||||
"Tee Ball Bat": ItemData("Ness Weapons", 0xEB0012, ItemClassification.filler, 0),
|
||||
"Sand Lot Bat": ItemData("Ness Weapons", 0xEB0013, ItemClassification.filler, 0),
|
||||
"Minor League Bat": ItemData("Ness Weapons", 0xEB0014, ItemClassification.filler, 0),
|
||||
"Mr. Baseball Bat": ItemData("Ness Weapons", 0xEB0015, ItemClassification.useful, 0),
|
||||
"Big League Bat": ItemData("Ness Weapons", 0xEB00D5, ItemClassification.useful, 0),
|
||||
"Hall of Fame Bat": ItemData("Ness Weapons", 0xEB0017, ItemClassification.useful, 0),
|
||||
"Magicant Bat": ItemData("Ness Weapons", 0xEB0018, ItemClassification.useful),
|
||||
"Legendary Bat": ItemData("Ness Weapons", 0xEB0019, ItemClassification.useful),
|
||||
"Gutsy Bat": ItemData("Ness Weapons", 0xEB001A, ItemClassification.useful, 0),
|
||||
"Casey Bat": ItemData("Ness Weapons", 0xEB001B, ItemClassification.filler, 0),
|
||||
|
||||
"Fry Pan": ItemData("Paula Weapons", 0xEB001C, ItemClassification.filler, 0),
|
||||
"Thick Fry Pan": ItemData("Paula Weapons", 0xEB001D, ItemClassification.filler, 0),
|
||||
"Deluxe Fry Pan": ItemData("Paula Weapons", 0xEB001E, ItemClassification.filler, 0),
|
||||
"Chef's Fry Pan": ItemData("Paula Weapons", 0xEB001F, ItemClassification.useful, 0),
|
||||
"French Fry Pan": ItemData("Paula Weapons", 0xEB0020, ItemClassification.useful, 0),
|
||||
"Magic Fry Pan": ItemData("Paula Weapons", 0xEB0021, ItemClassification.useful, 0),
|
||||
"Holy Fry Pan": ItemData("Paula Weapons", 0xEB0022, ItemClassification.useful, 0),
|
||||
|
||||
"Sword of Kings": ItemData("Poo Weapons", 0xEB0023, ItemClassification.useful, 0),
|
||||
|
||||
"Pop Gun": ItemData("Jeff Weapons", 0xEB0024, ItemClassification.filler),
|
||||
"Stun Gun": ItemData("Jeff Weapons", 0xEB0025, ItemClassification.filler),
|
||||
"Toy Air Gun": ItemData("Jeff Weapons", 0xEB0026, ItemClassification.filler, 0),
|
||||
"Magnum Air Gun": ItemData("Jeff Weapons", 0xEB0027, ItemClassification.filler, 0),
|
||||
"Zip Gun": ItemData("Jeff Weapons", 0xEB0028, ItemClassification.filler, 0),
|
||||
"Laser Gun": ItemData("Jeff Weapons", 0xEB0029, ItemClassification.filler, 0),
|
||||
"Hyper Beam": ItemData("Jeff Weapons", 0xEB002A, ItemClassification.useful, 0),
|
||||
"Crusher Beam": ItemData("Jeff Weapons", 0xEB002B, ItemClassification.useful, 0),
|
||||
"Spectrum Beam": ItemData("Jeff Weapons", 0xEB002C, ItemClassification.useful, 0),
|
||||
"Death Ray": ItemData("Jeff Weapons", 0xEB002D, ItemClassification.useful),
|
||||
"Baddest Beam": ItemData("Jeff Weapons", 0xEB002E, ItemClassification.useful, 0),
|
||||
"Moon Beam Gun": ItemData("Jeff Weapons", 0xEB002F, ItemClassification.useful),
|
||||
"Gaia Beam": ItemData("Jeff Weapons", 0xEB0030, ItemClassification.useful, 0),
|
||||
|
||||
"Yo-yo": ItemData("Alt Weapons", 0xEB0031, ItemClassification.filler, 0),
|
||||
"Slingshot": ItemData("Alt Weapons", 0xEB0032, ItemClassification.filler, 0),
|
||||
"Bionic Slingshot": ItemData("Alt Weapons", 0xEB0033, ItemClassification.filler, 0),
|
||||
"Trick Yo-yo": ItemData("Alt Weapons", 0xEB0034, ItemClassification.filler, 0),
|
||||
"Combat Yo-yo": ItemData("Alt Weapons", 0xEB0035, ItemClassification.filler, 0),
|
||||
|
||||
"Travel Charm": ItemData("Body Equipment", 0xEB0036, ItemClassification.filler),
|
||||
"Great Charm": ItemData("Body Equipment", 0xEB0037, ItemClassification.filler),
|
||||
"Crystal Charm": ItemData("Body Equipment", 0xEB0038, ItemClassification.filler, 0),
|
||||
"Rabbit's Foot": ItemData("Body Equipment", 0xEB0039, ItemClassification.useful),
|
||||
"Flame Pendant": ItemData("Body Equipment", 0xEB003A, ItemClassification.useful),
|
||||
"Rain Pendant": ItemData("Body Equipment", 0xEB003B, ItemClassification.useful),
|
||||
"Night Pendant": ItemData("Body Equipment", 0xEB003C, ItemClassification.useful),
|
||||
"Sea Pendant": ItemData("Body Equipment", 0xEB003D, ItemClassification.useful),
|
||||
"Star Pendant": ItemData("Body Equipment", 0xEB003E, ItemClassification.useful, 0),
|
||||
"Cloak of Kings": ItemData("Poo Equipment", 0xEB003F, ItemClassification.useful),
|
||||
|
||||
"Cheap Bracelet": ItemData("Arm Equipment", 0xEB0040, ItemClassification.filler, 0),
|
||||
"Copper Bracelet": ItemData("Arm Equipment", 0xEB0041, ItemClassification.filler, 0),
|
||||
"Silver Bracelet": ItemData("Arm Equipment", 0xEB0042, ItemClassification.filler, 0),
|
||||
"Gold Bracelet": ItemData("Arm Equipment", 0xEB0043, ItemClassification.filler, 0),
|
||||
"Platinum Band": ItemData("Arm Equipment", 0xEB00D8, ItemClassification.useful),
|
||||
"Diamond Band": ItemData("Arm Equipment", 0xEB00D9, ItemClassification.useful),
|
||||
"Pixie's Bracelet": ItemData("Arm Equipment", 0xEB0046, ItemClassification.useful),
|
||||
"Cherub's Band": ItemData("Arm Equipment", 0xEB0047, ItemClassification.useful),
|
||||
"Goddess Band": ItemData("Arm Equipment", 0xEB0048, ItemClassification.useful),
|
||||
"Bracer of Kings": ItemData("Poo Equipment", 0xEB0049, ItemClassification.useful),
|
||||
|
||||
"Baseball Cap": ItemData("Other Equipment", 0xEB004A, ItemClassification.filler, 0),
|
||||
"Holmes Hat": ItemData("Other Equipment", 0xEB004B, ItemClassification.filler, 0),
|
||||
"Mr. Baseball Cap": ItemData("Other Equipment", 0xEB004C, ItemClassification.filler, 0),
|
||||
"Hard Hat": ItemData("Other Equipment", 0xEB004D, ItemClassification.filler, 0),
|
||||
"Ribbon": ItemData("Ribbons", 0xEB004E, ItemClassification.filler, 0),
|
||||
"Red Ribbon": ItemData("Ribbons", 0xEB004F, ItemClassification.filler, 0),
|
||||
"Goddess Ribbon": ItemData("Ribbons", 0xEB0050, ItemClassification.useful, 0),
|
||||
"Coin of Slumber": ItemData("Other Equipment", 0xEB0051, ItemClassification.useful),
|
||||
"Coin of Defense": ItemData("Other Equipment", 0xEB0052, ItemClassification.useful, 0),
|
||||
"Lucky Coin": ItemData("Other Equipment", 0xEB0053, ItemClassification.useful, 0),
|
||||
"Talisman Coin": ItemData("Other Equipment", 0xEB0054, ItemClassification.useful, 0),
|
||||
"Shiny Coin": ItemData("Other Equipment", 0xEB0055, ItemClassification.useful, 0),
|
||||
"Souvenir Coin": ItemData("Other Equipment", 0xEB0056, ItemClassification.useful),
|
||||
"Diadem of Kings": ItemData("Poo Equipment", 0xEB0057, ItemClassification.useful),
|
||||
|
||||
"Cookie": ItemData("Food", 0xEB0058, ItemClassification.filler, 0),
|
||||
"Bag of Fries": ItemData("Food", 0xEB0059, ItemClassification.filler, 0),
|
||||
"Hamburger": ItemData("Food", 0xEB005A, ItemClassification.filler, 0),
|
||||
"Boiled Egg": ItemData("Food", 0xEB005B, ItemClassification.filler, 0),
|
||||
"Fresh Egg": ItemData("Food", 0xEB005C, ItemClassification.filler, 0),
|
||||
"Picnic Lunch": ItemData("Food", 0xEB005D, ItemClassification.filler, 0),
|
||||
"Pasta di Summers": ItemData("Food", 0xEB005E, ItemClassification.filler, 0),
|
||||
"Pizza": ItemData("Food", 0xEB005F, ItemClassification.filler, 0),
|
||||
"Chef's Special": ItemData("Food", 0xEB0060, ItemClassification.filler, 0),
|
||||
"Large Pizza": ItemData("Food", 0xEB0061, ItemClassification.filler, 0),
|
||||
"PSI Caramel": ItemData("Food", 0xEB0062, ItemClassification.useful, 0),
|
||||
"Magic Truffle": ItemData("Food", 0xEB0063, ItemClassification.useful, 0),
|
||||
"Brain Food Lunch": ItemData("Food", 0xEB0064, ItemClassification.useful, 0),
|
||||
"Rock Candy": ItemData("Food", 0xEB0065, ItemClassification.useful, 0),
|
||||
"Croissant": ItemData("Food", 0xEB0066, ItemClassification.filler, 0),
|
||||
"Bread Roll": ItemData("Food", 0xEB0067, ItemClassification.filler, 0),
|
||||
|
||||
"Pak of Bubble Gum": ItemData("Key Items", 0xEB0068, ItemClassification.progression),
|
||||
"Jar of Fly Honey": ItemData("Key Items", 0xEB0069, ItemClassification.progression),
|
||||
|
||||
"Can of Fruit Juice": ItemData("Food", 0xEB006A, ItemClassification.filler, 0),
|
||||
"Royal Iced Tea": ItemData("Food", 0xEB006B, ItemClassification.filler, 0),
|
||||
"Protein Drink": ItemData("Food", 0xEB006C, ItemClassification.filler, 0),
|
||||
"Kraken Soup": ItemData("Food", 0xEB006D, ItemClassification.filler, 0),
|
||||
"Bottle of Water": ItemData("Food", 0xEB006E, ItemClassification.filler, 0),
|
||||
"Cold Remedy": ItemData("Status Heal", 0xEB006F, ItemClassification.filler, 0),
|
||||
"Vial of Serum": ItemData("Status Heal", 0xEB0070, ItemClassification.filler, 0),
|
||||
"IQ Capsule": ItemData("Food", 0xEB0071, ItemClassification.useful, 0),
|
||||
"Guts Capsule": ItemData("Food", 0xEB0072, ItemClassification.useful, 0),
|
||||
"Speed Capsule": ItemData("Food", 0xEB0073, ItemClassification.useful, 0),
|
||||
"Vital Capsule": ItemData("Food", 0xEB0074, ItemClassification.useful, 0),
|
||||
"Luck Capsule": ItemData("Food", 0xEB0075, ItemClassification.useful, 0),
|
||||
"Ketchup Packet": ItemData("Condiments", 0xEB0076, ItemClassification.filler, 0),
|
||||
"Sugar Packet": ItemData("Condiments", 0xEB0077, ItemClassification.filler, 0),
|
||||
"Tin of Cocoa": ItemData("Condiments", 0xEB0078, ItemClassification.filler, 0),
|
||||
"Carton of Cream": ItemData("Condiments", 0xEB0079, ItemClassification.filler, 0),
|
||||
"Sprig of Parsley": ItemData("Condiments", 0xEB007A, ItemClassification.filler, 0),
|
||||
"Jar of Hot Sauce": ItemData("Condiments", 0xEB007B, ItemClassification.filler, 0),
|
||||
"Salt Packet": ItemData("Condiments", 0xEB007C, ItemClassification.filler, 0),
|
||||
"Tiny Key": ItemData("Key Items", 0xEB007D, ItemClassification.progression), # Progressive Gun
|
||||
"Jar of Delisauce": ItemData("Condiments", 0xEB007E, ItemClassification.useful, 0),
|
||||
"Wet Towel": ItemData("Status Heal", 0xEB007F, ItemClassification.filler, 0),
|
||||
"Refreshing Herb": ItemData("Status Heal", 0xEB0080, ItemClassification.useful, 0),
|
||||
"Secret Herb": ItemData("Status Heal", 0xEB0081, ItemClassification.useful, 0),
|
||||
"Horn of Life": ItemData("Status Heal", 0xEB0082, ItemClassification.useful, 0),
|
||||
|
||||
"Counter-PSI Unit": ItemData("Jeff Items", 0xEB0083, ItemClassification.useful, 0),
|
||||
"Shield Killer": ItemData("Jeff Items", 0xEB0084, ItemClassification.useful, 0),
|
||||
"Bazooka": ItemData("Jeff Items", 0xEB0085, ItemClassification.useful, 0),
|
||||
"Heavy Bazooka": ItemData("Jeff Items", 0xEB0086, ItemClassification.useful, 0),
|
||||
"HP-Sucker": ItemData("Jeff Items", 0xEB0087, ItemClassification.useful),
|
||||
"Hungry HP-Sucker": ItemData("Jeff Items", 0xEB0088, ItemClassification.useful, 0),
|
||||
"Xterminator Spray": ItemData("Battle Items", 0xEB0089, ItemClassification.useful, 0),
|
||||
"Slime Generator": ItemData("Jeff Items", 0xEB008A, ItemClassification.useful, 0),
|
||||
"Yogurt Dispenser": ItemData("Key Items", 0xEB008B, ItemClassification.progression),
|
||||
|
||||
"Ruler": ItemData("Battle Items", 0xEB008C, ItemClassification.filler, 0),
|
||||
"Snake Bag": ItemData("Battle Items", 0xEB008D, ItemClassification.filler, 0),
|
||||
"Mummy Wrap": ItemData("Battle Items", 0xEB008E, ItemClassification.filler, 0),
|
||||
"Protractor": ItemData("Battle Items", 0xEB008F, ItemClassification.filler, 0),
|
||||
"Bottle Rocket": ItemData("Jeff Items", 0xEB0090, ItemClassification.filler, 0),
|
||||
"Big Bottle Rocket": ItemData("Jeff Items", 0xEB0091, ItemClassification.useful, 0),
|
||||
"Multi Bottle Rocket": ItemData("Jeff Items", 0xEB0092, ItemClassification.useful, 0),
|
||||
"Bomb": ItemData("Battle Items", 0xEB0093, ItemClassification.filler, 0),
|
||||
"Super Bomb": ItemData("Battle Items", 0xEB0094, ItemClassification.useful, 0),
|
||||
"Insecticide Spray": ItemData("Battle Items", 0xEB0095, ItemClassification.filler, 0),
|
||||
"Rust Promoter": ItemData("Battle Items", 0xEB0096, ItemClassification.filler, 0),
|
||||
"Rust Promoter DX": ItemData("Battle Items", 0xEB0097, ItemClassification.useful, 0),
|
||||
"Pair of Dirty Socks": ItemData("Battle Items", 0xEB0098, ItemClassification.filler, 0),
|
||||
"Stag Beetle": ItemData("Battle Items", 0xEB0099, ItemClassification.filler, 0),
|
||||
"Toothbrush": ItemData("Battle Items", 0xEB009A, ItemClassification.filler, 0),
|
||||
"Handbag Strap": ItemData("Battle Items", 0xEB009B, ItemClassification.filler, 0),
|
||||
"Pharaoh's Curse": ItemData("Battle Items", 0xEB009C, ItemClassification.filler, 0),
|
||||
"Defense Shower": ItemData("Battle Items", 0xEB009D, ItemClassification.useful, 0),
|
||||
|
||||
"UFO Engine": ItemData("Key Items", 0xEB009E, ItemClassification.progression),
|
||||
"Sudden Guts Pill": ItemData("Battle Items", 0xEB009F, ItemClassification.useful, 0),
|
||||
"Bag of Dragonite": ItemData("Battle Items", 0xEB00A0, ItemClassification.useful, 0),
|
||||
"Defense Spray": ItemData("Battle Items", 0xEB00A1, ItemClassification.filler, 0),
|
||||
|
||||
"Piggy Nose": ItemData("Key Items", 0xEB00A2, ItemClassification.progression),
|
||||
"For Sale Sign": ItemData("Field Items", 0xEB00A3, ItemClassification.filler),
|
||||
"Shyness Book": ItemData("Key Items", 0xEB00A4, ItemClassification.progression),
|
||||
"Picture Postcard": ItemData("Field Items", 0xEB00A5, ItemClassification.filler, 0),
|
||||
"King Banana": ItemData("Key Items", 0xEB00A6, ItemClassification.progression),
|
||||
"Letter For Tony": ItemData("Key Items", 0xEB00A7, ItemClassification.progression),
|
||||
"Chick": ItemData("Field Items", 0xEB00A8, ItemClassification.filler, 0),
|
||||
"Chicken": ItemData("Field Items", 0xEB00A9, ItemClassification.filler, 0),
|
||||
"Key to the Shack": ItemData("Key Items", 0xEB00AA, ItemClassification.progression),
|
||||
"Key to the Cabin": ItemData("Key Items", 0xEB00AB, ItemClassification.progression),
|
||||
"Bad Key Machine": ItemData("Key Items", 0xEB00AC, ItemClassification.progression),
|
||||
# "Archipelago Item": ItemData("Key Items", 0xEB00AD, ItemClassification.progression, 0),
|
||||
"Zombie Paper": ItemData("Key Items", 0xEB00AE, ItemClassification.progression),
|
||||
"Hawk Eye": ItemData("Key Items", 0xEB00AF, ItemClassification.progression),
|
||||
"Bicycle": ItemData("Key Items", 0xEB00B0, ItemClassification.useful),
|
||||
"ATM Card": ItemData("Key Items", 0xEB00B1, ItemClassification.progression, 0),
|
||||
"Show Ticket": ItemData("Key Items", 0xEB00B2, ItemClassification.filler, 0),
|
||||
"Tenda Lavapants": ItemData("Key Items", 0xEB00B3, ItemClassification.progression), # Progressive Bat
|
||||
"Wad of Bills": ItemData("Key Items", 0xEB00B4, ItemClassification.progression),
|
||||
"Warp Pad": ItemData("Key Items", 0xEB00B5, ItemClassification.progression, 0),
|
||||
"Diamond": ItemData("Key Items", 0xEB00B6, ItemClassification.progression),
|
||||
"Signed Banana": ItemData("Key Items", 0xEB00B7, ItemClassification.progression),
|
||||
"Pencil Eraser": ItemData("Key Items", 0xEB00B8, ItemClassification.progression),
|
||||
"Hieroglyph Copy": ItemData("Key Items", 0xEB00B9, ItemClassification.progression),
|
||||
"Meteotite": ItemData("Field Items", 0xEB00BA, ItemClassification.useful, 0),
|
||||
"Contact Lens": ItemData("Key Items", 0xEB00BB, ItemClassification.progression),
|
||||
|
||||
"Hand-Aid": ItemData("Food", 0xEB00BC, ItemClassification.useful),
|
||||
"Trout Yogurt": ItemData("Food", 0xEB00BD, ItemClassification.filler, 0),
|
||||
"Banana": ItemData("Food", 0xEB00BE, ItemClassification.filler, 0),
|
||||
"Calorie Stick": ItemData("Food", 0xEB00BF, ItemClassification.filler, 0),
|
||||
"Key to the Tower": ItemData("Key Items", 0xEB00C0, ItemClassification.progression),
|
||||
"Meteorite Piece": ItemData("Key Items", 0xEB00C1, ItemClassification.progression),
|
||||
|
||||
"Earth Pendant": ItemData("Body Equipment", 0xEB00C2, ItemClassification.useful, 0),
|
||||
"Neutralizer": ItemData("Jeff Items", 0xEB00C3, ItemClassification.useful),
|
||||
"Sound Stone": ItemData("Key Items", 0xEB00C4, ItemClassification.progression, 0),
|
||||
"Exit Mouse": ItemData("Key Items", 0xEB00C5, ItemClassification.useful, 0),
|
||||
|
||||
"Gelato de Resort": ItemData("Food", 0xEB00C6, ItemClassification.filler, 0),
|
||||
"Snake": ItemData("Battle Items", 0xEB00C7, ItemClassification.filler, 0),
|
||||
"Viper": ItemData("Battle Items", 0xEB00C8, ItemClassification.filler, 0),
|
||||
"Brain Stone": ItemData("Battle Items", 0xEB00C9, ItemClassification.filler),
|
||||
"Police Badge": ItemData("Key Items", 0xEB00CA, ItemClassification.progression),
|
||||
"Mining Permit": ItemData("Key Items", 0xEB00CB, ItemClassification.progression),
|
||||
"Suporma": ItemData("Field Items", 0xEB00CC, ItemClassification.trap),
|
||||
"Key to the Locker": ItemData("Key Items", 0xEB00CD, ItemClassification.progression),
|
||||
"Insignificant Item": ItemData("Key Items", 0xEB00CE, ItemClassification.progression),
|
||||
"Magic Tart": ItemData("Food", 0xEB00CF, ItemClassification.useful, 0),
|
||||
"Tiny Ruby": ItemData("Key Items", 0xEB00D0, ItemClassification.progression),
|
||||
"Monkey's Love": ItemData("Battle Items", 0xEB00D1, ItemClassification.useful),
|
||||
"Eraser Eraser": ItemData("Key Items", 0xEB00D2, ItemClassification.progression),
|
||||
"Tendakraut": ItemData("Key Items", 0xEB00D3, ItemClassification.progression),
|
||||
|
||||
"T-Rex's Bat": ItemData("Ness Weapons", 0xEB00D4, ItemClassification.useful, 0),
|
||||
# "Big League Bat": ItemData("Ness Weapons", 0xEB0016, ItemClassification.useful, 0), Summers copy
|
||||
"Ultimate Bat": ItemData("Ness Weapons", 0xEB00D6, ItemClassification.useful, 0),
|
||||
"Double Beam": ItemData("Jeff Weapons", 0xEB00D7, ItemClassification.useful, 0),
|
||||
# "Platinum Band": ItemData("Arm Equipment", 0xEB00D8, ItemClassification.useful, 0), Summers copy
|
||||
# "Diamond Band": ItemData("Arm Equipment", 0xEB00D9, ItemClassification.useful, 0), Summers Copy
|
||||
"Defense Ribbon": ItemData("Ribbons", 0xEB00DA, ItemClassification.useful, 0),
|
||||
"Talisman Ribbon": ItemData("Ribbons", 0xEB00DB, ItemClassification.useful),
|
||||
"Saturn Ribbon": ItemData("Ribbons", 0xEB00DC, ItemClassification.useful),
|
||||
"Coin of Silence": ItemData("Other Equipment", 0xEB00DD, ItemClassification.useful, 0),
|
||||
"Charm Coin": ItemData("Other Equipment", 0xEB00DE, ItemClassification.useful, 0),
|
||||
|
||||
"Cup of Noodles": ItemData("Food", 0xEB00DF, ItemClassification.filler, 0),
|
||||
"Repel Sandwich": ItemData("Food", 0xEB00E0, ItemClassification.useful, 0),
|
||||
"Repel Superwich": ItemData("Food", 0xEB00E1, ItemClassification.useful, 0),
|
||||
"Lucky Sandwich": ItemData("Food", 0xEB00E2, ItemClassification.useful, 0),
|
||||
"Progressive Bat": ItemData("Progressive Equipment", 0xEB00E3, ItemClassification.useful, 0),
|
||||
"Progressive Fry Pan": ItemData("Progressive Equipment", 0xEB00E4, ItemClassification.useful, 0),
|
||||
"Progressive Gun": ItemData("Progressive Equipment", 0xEB00E5, ItemClassification.useful, 0),
|
||||
"Progressive Bracelet": ItemData("Progressive Equipment", 0xEB00E6, ItemClassification.useful, 0),
|
||||
"Progressive Other": ItemData("Progressive Equipment", 0xEB00E7, ItemClassification.useful, 0),
|
||||
|
||||
"Cup of Coffee": ItemData("Food", 0xEB00E8, ItemClassification.filler, 0),
|
||||
"Double Burger": ItemData("Food", 0xEB00E9, ItemClassification.filler, 0),
|
||||
"Peanut Cheese Bar": ItemData("Food", 0xEB00EA, ItemClassification.filler, 0),
|
||||
"Piggy Jelly": ItemData("Food", 0xEB00EB, ItemClassification.filler, 0),
|
||||
"Bowl of Rice Gruel": ItemData("Food", 0xEB00EC, ItemClassification.filler, 0),
|
||||
"Bean Croquette": ItemData("Food", 0xEB00ED, ItemClassification.filler, 0),
|
||||
"Molokheiya Soup": ItemData("Food", 0xEB00EE, ItemClassification.filler, 0),
|
||||
"Plain Roll": ItemData("Food", 0xEB00EF, ItemClassification.filler, 0),
|
||||
"Kabob": ItemData("Food", 0xEB00F0, ItemClassification.filler, 0),
|
||||
"Plain Yogurt": ItemData("Food", 0xEB00F1, ItemClassification.filler, 0),
|
||||
"Beef Jerky": ItemData("Food", 0xEB00F2, ItemClassification.filler, 0),
|
||||
"Mammoth Burger": ItemData("Food", 0xEB00F3, ItemClassification.filler, 0),
|
||||
"Spicy Jerky": ItemData("Food", 0xEB00F4, ItemClassification.filler, 0),
|
||||
"Luxury Jerky": ItemData("Food", 0xEB00F5, ItemClassification.filler, 0),
|
||||
"Bottle of DXwater": ItemData("Food", 0xEB00F6, ItemClassification.useful, 0),
|
||||
"Magic Pudding": ItemData("Food", 0xEB00F7, ItemClassification.useful, 0),
|
||||
|
||||
"Non-Stick Frypan": ItemData("Paula Weapons", 0xEB00F8, ItemClassification.useful, 0),
|
||||
"Mr. Saturn Coin": ItemData("Other Equipment", 0xEB00F9, ItemClassification.useful),
|
||||
"Meteornium": ItemData("Field Items", 0xEB00FA, ItemClassification.useful, 0),
|
||||
"Popsicle": ItemData("Food", 0xEB00FB, ItemClassification.filler, 0),
|
||||
"Cup of Lifenoodles": ItemData("Status Heal", 0xEB00FC, ItemClassification.useful, 0),
|
||||
"Carrot Key": ItemData("Key Items", 0xEB00FD, ItemClassification.progression),
|
||||
|
||||
"Onett Teleport": ItemData("PSI", 0xEB00FE, ItemClassification.progression),
|
||||
"Twoson Teleport": ItemData("PSI", 0xEB00FF, ItemClassification.progression),
|
||||
"Happy-Happy Village Teleport": ItemData("PSI", 0xEB0100, ItemClassification.progression),
|
||||
"Threed Teleport": ItemData("PSI", 0xEB0101, ItemClassification.progression),
|
||||
"Saturn Valley Teleport": ItemData("PSI", 0xEB0102, ItemClassification.progression),
|
||||
"Dusty Dunes Teleport": ItemData("PSI", 0xEB0103, ItemClassification.progression),
|
||||
"Fourside Teleport": ItemData("PSI", 0xEB0104, ItemClassification.progression),
|
||||
"Winters Teleport": ItemData("PSI", 0xEB0105, ItemClassification.progression),
|
||||
"Summers Teleport": ItemData("PSI", 0xEB0106, ItemClassification.progression),
|
||||
"Scaraba Teleport": ItemData("PSI", 0xEB0107, ItemClassification.progression),
|
||||
"Dalaam Teleport": ItemData("PSI", 0xEB0108, ItemClassification.progression),
|
||||
"Deep Darkness Teleport": ItemData("PSI", 0xEB0109, ItemClassification.progression),
|
||||
"Tenda Village Teleport": ItemData("PSI", 0xEB010A, ItemClassification.progression),
|
||||
"Lost Underworld Teleport": ItemData("PSI", 0xEB010B, ItemClassification.progression),
|
||||
"Progressive Poo PSI": ItemData("PSI", 0xEB010C, ItemClassification.useful, 2),
|
||||
"Magicant Teleport": ItemData("PSI", 0xEB010D, ItemClassification.progression),
|
||||
|
||||
"Paula": ItemData("Characters", 0xEB010E, ItemClassification.progression),
|
||||
"Jeff": ItemData("Characters", 0xEB010F, ItemClassification.progression),
|
||||
"Poo": ItemData("Characters", 0xEB0110, ItemClassification.progression),
|
||||
"Flying Man": ItemData("Characters", 0xEB0111, ItemClassification.useful),
|
||||
"Ness": ItemData("Characters", 0xEB0112, ItemClassification.progression),
|
||||
"Photograph": ItemData("Photos", 0xEB0113, ItemClassification.trap, 0),
|
||||
|
||||
"$10": ItemData("Money", 0xEB0114, ItemClassification.filler, 0),
|
||||
"$100": ItemData("Money", 0xEB0115, ItemClassification.filler, 0),
|
||||
"$1000": ItemData("Money", 0xEB0116, ItemClassification.useful, 0),
|
||||
|
||||
'Threed Tunnels Clear': ItemData('Events', None, ItemClassification.progression, 0),
|
||||
'Submarine to Deep Darkness': ItemData('Events', None, ItemClassification.progression, 0),
|
||||
'Melody': ItemData('Events', None, ItemClassification.progression, 0),
|
||||
'Saved Earth': ItemData('Events', None, ItemClassification.progression, 0),
|
||||
"Power of the Earth": ItemData("Events", None, ItemClassification.progression, 0),
|
||||
"Alternate Goal": ItemData("Events", None, ItemClassification.useful, 0),
|
||||
"Valley Bridge Repair": ItemData("Events", None, ItemClassification.progression, 0),
|
||||
"Magicant Unlock": ItemData("Events", None, ItemClassification.progression, 0),
|
||||
"ATM Access": ItemData("Events", None, ItemClassification.progression, 0)
|
||||
}
|
||||
|
||||
|
||||
def get_item_names_per_category() -> Dict[str, Set[str]]:
|
||||
categories: Dict[str, Set[str]] = {}
|
||||
|
||||
for name, data in item_table.items():
|
||||
if data.category != "Events":
|
||||
categories.setdefault(data.category, set()).add(name)
|
||||
|
||||
return categories
|
||||
594
worlds/earthbound/Locations.py
Normal file
594
worlds/earthbound/Locations.py
Normal file
@@ -0,0 +1,594 @@
|
||||
from typing import List, Optional, NamedTuple, TYPE_CHECKING
|
||||
from .Options import MagicantMode, ShopRandomizer
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import EarthBoundWorld
|
||||
|
||||
|
||||
class LocationData(NamedTuple):
|
||||
region: str
|
||||
name: str
|
||||
code: Optional[int]
|
||||
|
||||
|
||||
def get_locations(world: "EarthBoundWorld") -> List[LocationData]:
|
||||
|
||||
location_table: List[LocationData] = [
|
||||
LocationData("Northern Onett", "Onett - Tracy Gift", 0xEB0000),
|
||||
LocationData("Northern Onett", "Onett - Tracy's Room Present", 0xEB0001),
|
||||
LocationData("Northern Onett", "Onett - Hilltop Present", 0xEB0002),
|
||||
LocationData("Northern Onett", "Onett - Meteor Item", 0xEB0003),
|
||||
LocationData("Northern Onett", "Onett - Buzz Buzz", 0xEB0004),
|
||||
LocationData("Northern Onett", "Onett - Mani Mani Statue", 0xEB0005),
|
||||
LocationData("Onett", "Onett - Library Counter", 0xEB0006),
|
||||
LocationData("Onett", "Onett - Library Bookshelf", 0xEB0007),
|
||||
LocationData("Onett", "Onett - Burger Shop Trashcan", 0xEB0008),
|
||||
LocationData("Onett", "Onett - Treehouse Guy", 0xEB0009),
|
||||
LocationData("Onett", "Onett - South Road Present", 0xEB000A),
|
||||
LocationData("Onett", "Onett - Hotel Trashcan", 0xEB000B),
|
||||
LocationData("Onett", "Onett - Arcade Trashcan", 0xEB000C),
|
||||
LocationData("Onett", "Onett - Mayor Pirkle", 0xEB000D),
|
||||
LocationData("Onett", "Onett - Traveling Entertainer", 0xEB000E),
|
||||
LocationData("Giant Step", "Giant Step - First Cave Present", 0xEB000F),
|
||||
LocationData("Giant Step", "Giant Step - Floor 2 Cave Present", 0xEB0010),
|
||||
LocationData("Giant Step", "Giant Step - Floor 3 Present", 0xEB0011),
|
||||
LocationData("Twoson", "Twoson - Bike Shop Rental", 0xEB0012),
|
||||
LocationData("Twoson", "Twoson - Antique Shop", 0xEB0013),
|
||||
LocationData("Twoson", "Twoson - Paula's Room Present", 0xEB0014),
|
||||
LocationData("Twoson", "Twoson - Apple Kid Trashcan", 0xEB0015),
|
||||
LocationData("Twoson", "Twoson - South of Town Present", 0xEB0016),
|
||||
LocationData("Twoson", "Twoson - Orange Kid Donation", 0xEB0017),
|
||||
LocationData("Twoson", "Twoson - Apple Kid Invention", 0xEB0018),
|
||||
LocationData("Twoson", "Twoson - Apple Kid's Mouse", 0xEB0019),
|
||||
LocationData("Twoson", "Twoson - Paula's Mother", 0xEB001A),
|
||||
LocationData("Everdred's House", "Twoson - Everdred Meeting", 0xEB001B),
|
||||
LocationData("Twoson", "Twoson - Insignificant Location", 0xEB001C),
|
||||
LocationData("Peaceful Rest Valley", "Peaceful Rest Valley - Split Hill Present", 0xEB001D),
|
||||
LocationData("Peaceful Rest Valley", "Peaceful Rest Valley - Hill Nook Present", 0xEB001E),
|
||||
LocationData("Peaceful Rest Valley", "Peaceful Rest Valley - South of Bridge Present", 0xEB001F),
|
||||
LocationData("Peaceful Rest Valley", "Peaceful Rest Valley - Dead End Present", 0xEB0020),
|
||||
LocationData("Peaceful Rest Valley", "Peaceful Rest Valley - River Overlook Present", 0xEB0021),
|
||||
LocationData("Peaceful Rest Valley", "Peaceful Rest Valley - North Side Present", 0xEB0022),
|
||||
LocationData("Happy-Happy Village", "Happy-Happy Village - Donation Lady", 0xEB0023),
|
||||
LocationData("Happy-Happy HQ", "Happy-Happy Village - Right HQ Present", 0xEB0024),
|
||||
LocationData("Happy-Happy HQ", "Happy-Happy Village - Left HQ Present", 0xEB0025),
|
||||
LocationData("Happy-Happy Village", "Happy-Happy Village - Prisoner Item", 0xEB0026),
|
||||
LocationData("Happy-Happy Village", "Happy-Happy Village - Prisoner", 0xEB0027),
|
||||
LocationData("Happy-Happy HQ", "Happy-Happy Village - Defeat Carpainter", 0xEB0028),
|
||||
LocationData("Lilliput Steps", "Lilliput Steps - Southwest Pool Present", 0xEB0029),
|
||||
LocationData("Lilliput Steps", "Lilliput Steps - East Cliff Present", 0xEB002A),
|
||||
LocationData("Lilliput Steps", "Lilliput Steps - North Stream Present", 0xEB002B),
|
||||
LocationData("Boogey Tent", "Threed - Boogey Tent Trashcan", 0xEB002C),
|
||||
LocationData("Threed", "Threed - Cemetery Trashcan", 0xEB002D),
|
||||
LocationData("Threed", "Threed - Downtown Trashcan", 0xEB002E),
|
||||
LocationData("Threed", "Threed - East Side Trashcan", 0xEB002F),
|
||||
LocationData("Threed", "Threed - Northeast Shack Trashcan", 0xEB0030),
|
||||
LocationData("Threed", "Threed - Hospital Drawer", 0xEB0031),
|
||||
LocationData("Threed", "Threed - Zombie Prisoner", 0xEB0032),
|
||||
LocationData("Threed Underground", "Threed Underground - Left Coffin", 0xEB0033),
|
||||
LocationData("Threed Underground", "Threed Underground - Right Coffin", 0xEB0034),
|
||||
LocationData("Grapefruit Falls", "Grapefruit Falls - South Present", 0xEB0035),
|
||||
LocationData("Grapefruit Falls", "Grapefruit Falls - North Present", 0xEB0036),
|
||||
LocationData("Grapefruit Falls", "Grapefruit Falls - Saturn Cave Present", 0xEB0037),
|
||||
LocationData("Saturn Valley", "Saturn Valley - Ladder Present", 0xEB0038),
|
||||
LocationData("Saturn Valley", "Saturn Valley - Trashcan #1", 0xEB0039),
|
||||
LocationData("Saturn Valley", "Saturn Valley - Trashcan #2", 0xEB003A),
|
||||
LocationData("Saturn Valley", "Saturn Valley - Trashcan #3", 0xEB003B),
|
||||
LocationData("Upper Saturn Valley", "Saturn Valley - Saturn Coffee", 0xEB003C),
|
||||
LocationData("Saturn Valley", "Saturn Valley - Post Belch Gift #1", 0xEB003D),
|
||||
LocationData("Saturn Valley", "Saturn Valley - Post Belch Gift #2", 0xEB003E),
|
||||
LocationData("Saturn Valley", "Saturn Valley - Post Belch Gift #3", 0xEB003F),
|
||||
LocationData("Milky Well", "Milky Well - Cavern Present", 0xEB0040),
|
||||
LocationData("Belch's Factory", "Belch's Factory - Top Right Room Trashcan", 0xEB0041),
|
||||
LocationData("Belch's Factory", "Belch's Factory - Pit Room Trashcan #1", 0xEB0042),
|
||||
LocationData("Belch's Factory", "Belch's Factory - Pit Room Trashcan #2", 0xEB0043),
|
||||
LocationData("Belch's Factory", "Belch's Factory - Balcony Room Trashcan #1", 0xEB0044),
|
||||
LocationData("Belch's Factory", "Belch's Factory - Balcony Room Trashcan #2", 0xEB0045),
|
||||
LocationData("Belch's Factory", "Belch's Factory - Balcony Room Trashcan #3", 0xEB0046),
|
||||
LocationData("Dusty Dunes Desert", "Dusty Dunes - Northwest Corner Present", 0xEB0047),
|
||||
LocationData("Dusty Dunes Desert", "Dusty Dunes - South Side Present", 0xEB0048),
|
||||
LocationData("Dusty Dunes Desert", "Dusty Dunes - Surrounding Rocks Present", 0xEB0049),
|
||||
LocationData("Dusty Dunes Desert", "Dusty Dunes - Black Sesame Present", 0xEB004A),
|
||||
LocationData("Dusty Dunes Desert", "Dusty Dunes - Oasis Present", 0xEB004B),
|
||||
LocationData("Dusty Dunes Desert", "Dusty Dunes - Northeast Corner Present", 0xEB004C),
|
||||
LocationData("Dusty Dunes Desert", "Dusty Dunes - North Central Present", 0xEB004D),
|
||||
LocationData("Dusty Dunes Desert", "Dusty Dunes - Shining Spot", 0xEB004E),
|
||||
LocationData("Dusty Dunes Desert", "Dusty Dunes - East Peninsula Present", 0xEB004F),
|
||||
LocationData("Dusty Dunes Desert", "Dusty Dunes - Mine Reward", 0xEB0050),
|
||||
LocationData("Snow Wood Boarding School", "Snow Wood - Many Present Room Present #1", 0xEB0051),
|
||||
LocationData("Snow Wood Boarding School", "Snow Wood - Many Present Room Present #2", 0xEB0052),
|
||||
LocationData("Snow Wood Boarding School", "Snow Wood - Many Present Room Present #3", 0xEB0053),
|
||||
LocationData("Snow Wood Boarding School", "Snow Wood - Many Present Room Present #4", 0xEB0054),
|
||||
LocationData("Snow Wood Boarding School", "Snow Wood - Many Present Room Present #5", 0xEB0055),
|
||||
LocationData("Snow Wood Boarding School", "Snow Wood - Many Present Room Present #6", 0xEB0056),
|
||||
LocationData("Snow Wood Boarding School", "Snow Wood - Many Present Room Present #7", 0xEB0057),
|
||||
LocationData("Scaraba", "Scaraba - Snake Bag Salesman", 0xEB0058),
|
||||
LocationData("Snow Wood Boarding School", "Snow Wood - Upper Right Locker", 0xEB0059),
|
||||
LocationData("Snow Wood Boarding School", "Snow Wood - Upper Left Locker", 0xEB005A),
|
||||
LocationData("Snow Wood Boarding School", "Snow Wood - Bottom Right Locker", 0xEB005B),
|
||||
LocationData("Snow Wood Boarding School", "Snow Wood - Bottom Left Locker", 0xEB005C),
|
||||
LocationData("Snow Wood Boarding School", "Snow Wood - Maxwell Item", 0xEB005D),
|
||||
LocationData("Snow Wood Boarding School", "Snow Wood - Bedroom", 0xEB005E),
|
||||
LocationData("Winters", "Winters - Drugstore Saleswoman", 0xEB005F),
|
||||
LocationData("Brickroad Maze", "Brick Road Maze - Top Path Present", 0xEB0060),
|
||||
LocationData("Brickroad Maze", "Brick Road Maze - Guarded Present", 0xEB0061),
|
||||
LocationData("Brickroad Maze", "Brick Road Maze - Out of the Way Present", 0xEB0062),
|
||||
LocationData("Brickroad Maze", "Brick Road Maze - Alcove Present", 0xEB0063),
|
||||
LocationData("Brickroad Maze", "Brick Road Maze - Near Exit Present", 0xEB0064),
|
||||
LocationData("Rainy Circle", "Rainy Circle - Isolated Present", 0xEB0065),
|
||||
LocationData("Rainy Circle", "Rainy Circle - East Cliff Present", 0xEB0066),
|
||||
LocationData("Rainy Circle", "Rainy Circle - Near Ropes Present", 0xEB0067),
|
||||
LocationData("Andonuts Lab Area", "Andonuts Lab - Present", 0xEB0068),
|
||||
LocationData("Andonuts Lab Area", "Andonuts Lab - Mouse", 0xEB0069),
|
||||
LocationData("Stonehenge Base", "Stonehenge - Purple Maze Present", 0xEB006A),
|
||||
LocationData("Stonehenge Base", "Stonehenge - Dead End Present", 0xEB006B),
|
||||
LocationData("Stonehenge Base", "Stonehenge - Near End of the Maze Present", 0xEB006C),
|
||||
LocationData("Stonehenge Base", "Stonehenge - Bridge Room East Balcony Present", 0xEB006D),
|
||||
LocationData("Stonehenge Base", "Stonehenge - Bridge Room Lower Present", 0xEB006E),
|
||||
LocationData("Stonehenge Base", "Stonehenge - Flashing Room Right Path Present", 0xEB006F),
|
||||
LocationData("Stonehenge Base", "Stonehenge - Flashing Room Center Present", 0xEB0070),
|
||||
LocationData("Stonehenge Base", "Stonehenge - Flashing Room Upper Present", 0xEB0071),
|
||||
LocationData("Stonehenge Base", "Stonehenge - Kidnapped Mr. Saturn", 0xEB0072),
|
||||
LocationData("Stonehenge Base", "Stonehenge - Tony Item", 0xEB0073),
|
||||
LocationData("Gold Mine", "Gold Mine - Mouse Crossroad Present #1", 0xEB0074),
|
||||
LocationData("Gold Mine", "Gold Mine - Mouse Crossroad Present #2", 0xEB0075),
|
||||
LocationData("Gold Mine", "Gold Mine - B1F Lonely Mole Present", 0xEB0076),
|
||||
LocationData("Gold Mine", "Gold Mine - South Hall Present", 0xEB0077),
|
||||
LocationData("Gold Mine", "Gold Mine - South Corner Present", 0xEB0078),
|
||||
LocationData("Gold Mine", "Gold Mine - South Mole Present #1", 0xEB0079),
|
||||
LocationData("Gold Mine", "Gold Mine - South Mole Present #2", 0xEB007A),
|
||||
LocationData("Gold Mine", "Gold Mine - North Crossroad Detour Present", 0xEB007B),
|
||||
LocationData("Gold Mine", "Gold Mine - North Mole Present", 0xEB007C),
|
||||
LocationData("Gold Mine", "Gold Mine - West Mole Present", 0xEB007D),
|
||||
LocationData("Gold Mine", "Gold Mine - B1F Isolated Present", 0xEB007E),
|
||||
LocationData("Gold Mine", "Gold Mine - West Crossroad Detour Present", 0xEB007F),
|
||||
LocationData("Gold Mine", "Gold Mine - B1F Junction Present", 0xEB0080),
|
||||
LocationData("Gold Mine", "Gold Mine - B1F Junction Mole Present", 0xEB0081),
|
||||
LocationData("Monkey Caves", "Monkey Caves - 1F Right Chest", 0xEB00F1),
|
||||
LocationData("Monkey Caves", "Monkey Caves - 1F Left Chest", 0xEB00F2),
|
||||
LocationData("Monkey Caves", "Monkey Caves - West 2F Left Chest", 0xEB00F3),
|
||||
LocationData("Monkey Caves", "Monkey Caves - West 2F Right Chest #1", 0xEB00F4),
|
||||
LocationData("Monkey Caves", "Monkey Caves - West 2F Right Chest #2", 0xEB00F5),
|
||||
LocationData("Monkey Caves", "Monkey Caves - East 2F Left Chest", 0xEB00F6),
|
||||
LocationData("Monkey Caves", "Monkey Caves - East 2F Right Chest", 0xEB00F7),
|
||||
LocationData("Monkey Caves", "Monkey Caves - East West 3F Right Chest #1", 0xEB00F8),
|
||||
LocationData("Monkey Caves", "Monkey Caves - East West 3F Right Chest #2", 0xEB00F9),
|
||||
LocationData("Monkey Caves", "Monkey Caves - West End Chest", 0xEB0082),
|
||||
LocationData("Monkey Caves", "Monkey Caves - West End Trashcan", 0xEB0083),
|
||||
LocationData("Monkey Caves", "Monkey Caves - East End Chest", 0xEB0084),
|
||||
LocationData("Monkey Caves", "Monkey Caves - East End Trashcan", 0xEB0085),
|
||||
LocationData("Monkey Caves", "Monkey Caves - Bow Monkey Gift", 0xEB0086),
|
||||
LocationData("Monkey Caves", "Monkey Caves - Talah Rama Chest #1", 0xEB0087),
|
||||
LocationData("Monkey Caves", "Monkey Caves - Talah Rama Chest #2", 0xEB0088),
|
||||
LocationData("Monkey Caves", "Monkey Caves - Talah Rama Gift", 0xEB0089),
|
||||
LocationData("Monkey Caves", "Monkey Caves - Monkey Power", 0xEB008A),
|
||||
LocationData("Fourside", "Fourside - Venus Gift", 0xEB008B),
|
||||
LocationData("Moonside", "Fourside - Post-Moonside Delivery", 0xEB008C),
|
||||
LocationData("Fourside", "Fourside - Bakery 2F Gift", 0xEB008D),
|
||||
LocationData("Moonside", "Moonside - Two Trees Present", 0xEB008E),
|
||||
LocationData("Moonside", "Moonside - East Island Present", 0xEB008F),
|
||||
LocationData("Moonside", "Moonside - Businessman Present", 0xEB0090),
|
||||
LocationData("Moonside", "Moonside - West Island Present", 0xEB0091),
|
||||
LocationData("Moonside", "Moonside - Hospital Present", 0xEB0092),
|
||||
LocationData("Fourside Dept. Store", "Fourside - Department Store Blackout", 0xEB0093),
|
||||
LocationData("Magnet Hill", "Magnet Hill - West Entrance Trashcan", 0xEB0094),
|
||||
LocationData("Magnet Hill", "Magnet Hill - First Room Free Door Trashcan", 0xEB0095),
|
||||
LocationData("Magnet Hill", "Magnet Hill - First Room Barrel Door Trashcan", 0xEB0096),
|
||||
LocationData("Magnet Hill", "Magnet Hill - Second Room Dead End Trashcan", 0xEB0097),
|
||||
LocationData("Magnet Hill", "Magnet Hill - Final Room Door Trashcan", 0xEB0098),
|
||||
LocationData("Magnet Hill", "Fourside - Magnet Hill Chest", 0xEB0099),
|
||||
LocationData("Monotoli Building", "Monotoli Building - One Table Present", 0xEB009A),
|
||||
LocationData("Monotoli Building", "Monotoli Building - Two Table Present", 0xEB009B),
|
||||
LocationData("Monotoli Building", "Monotoli Building - Electra Gift", 0xEB009C),
|
||||
LocationData("Monotoli Building", "Monotoli Building - Monotoli Gift", 0xEB009D),
|
||||
LocationData("Monotoli Building", "Monotoli Building - Monotoli Character", 0xEB009E),
|
||||
LocationData("Summers Museum", "Summers - Museum Item", 0xEB009F),
|
||||
LocationData("Summers", "Summers - Magic Cake", 0xEB00A0),
|
||||
LocationData("Dalaam", "Dalaam - Throne Room Chest #1", 0xEB00A1),
|
||||
LocationData("Dalaam", "Dalaam - Throne Room Chest #2", 0xEB00A2),
|
||||
LocationData("Dalaam", "Dalaam - Throne Room Chest #3", 0xEB00A3),
|
||||
LocationData("Dalaam", "Dalaam - Trial of Mu", 0xEB00A4),
|
||||
LocationData("Dalaam", "Dalaam - Restaurant Chest #1", 0xEB00A5),
|
||||
LocationData("Dalaam", "Dalaam - Restaurant Chest #2", 0xEB00A6),
|
||||
LocationData("Dalaam", "Dalaam - Do Do Guy's House Chest", 0xEB00A7),
|
||||
LocationData("Dalaam", "Dalaam - Upper House Chest", 0xEB00A8),
|
||||
LocationData("Dalaam", "Dalaam - Throne Character", 0xEB00A9),
|
||||
LocationData("Ness's Mind", "Poo - Starting Item", 0xEB00AA),
|
||||
LocationData("Pink Cloud", "Pink Cloud - Three Holes Present", 0xEB00AB),
|
||||
LocationData("Pink Cloud", "Pink Cloud - Left Hole Present", 0xEB00AC),
|
||||
LocationData("Pink Cloud", "Pink Cloud - Ground Floor Present", 0xEB00AD),
|
||||
LocationData("Pyramid", "Pyramid - Anteroom Sarcophagus", 0xEB00AE),
|
||||
LocationData("Pyramid", "Pyramid - Northwest Door Sarcophagus", 0xEB00AF),
|
||||
LocationData("Pyramid", "Pyramid - Hallway Sarcophagus #1", 0xEB00B0),
|
||||
LocationData("Pyramid", "Pyramid - Hallway Sarcophagus #2", 0xEB00B1),
|
||||
LocationData("Pyramid", "Pyramid - Switch Room Sarcophagus", 0xEB00B2),
|
||||
LocationData("Pyramid", "Pyramid - Pedestal Item", 0xEB00B3),
|
||||
LocationData("Pyramid", "Pyramid - Way Out Sarcophagus", 0xEB00B4),
|
||||
LocationData("Southern Scaraba", "Scaraba - Star Master", 0xEB00B5),
|
||||
LocationData("Southern Scaraba", "Scaraba - Key Holder", 0xEB00B6),
|
||||
LocationData("Dungeon Man", "Dungeon Man - 1F Dead End Present", 0xEB00B7),
|
||||
LocationData("Dungeon Man", "Dungeon Man - 1F Long Walk Present", 0xEB00B8),
|
||||
LocationData("Dungeon Man", "Dungeon Man - 1F Disappointing Present", 0xEB00B9),
|
||||
LocationData("Dungeon Man", "Dungeon Man - 1F Opinion Present", 0xEB00BA),
|
||||
LocationData("Dungeon Man", "Dungeon Man - 1F No Sign Present", 0xEB00BB),
|
||||
LocationData("Dungeon Man", "Dungeon Man - 2F Unnecessary Billboard Present", 0xEB00BC),
|
||||
LocationData("Dungeon Man", "Dungeon Man - 2F Dungeon Exploration Present", 0xEB00BD),
|
||||
LocationData("Dungeon Man", "Dungeon Man - 2F South Ledge Present", 0xEB00BE),
|
||||
LocationData("Dungeon Man", "Dungeon Man - 2F North Alcove Present", 0xEB00BF),
|
||||
LocationData("Dungeon Man", "Dungeon Man - 3F Present", 0xEB00C0),
|
||||
LocationData("Dungeon Man", "Dungeon Man - 2F Hole Present", 0xEB00C1),
|
||||
LocationData("Dungeon Man", "Dungeon Man - 1F Exit Ledge Present", 0xEB00C2),
|
||||
LocationData("Deep Darkness", "Deep Darkness - Teleporting Monkey", 0xEB00C3),
|
||||
LocationData("Deep Darkness", "Deep Darkness - Crest of Darkness Present", 0xEB00C4),
|
||||
LocationData("Deep Darkness Darkness", "Deep Darkness - Helicopter Present", 0xEB00C5),
|
||||
LocationData("Deep Darkness Darkness", "Deep Darkness - Yellow Bird Present", 0xEB00C6),
|
||||
LocationData("Deep Darkness Darkness", "Deep Darkness - Swamp Present", 0xEB00C7),
|
||||
LocationData("Deep Darkness Darkness", "Deep Darkness - Corner Present", 0xEB00C8),
|
||||
LocationData("Deep Darkness Darkness", "Deep Darkness - Alcove Present", 0xEB00C9),
|
||||
LocationData("Deep Darkness Darkness", "Deep Darkness - North Alcove Truffle", 0xEB00CA),
|
||||
LocationData("Deep Darkness Darkness", "Deep Darkness - Near Land Truffle", 0xEB00CB),
|
||||
LocationData("Deep Darkness Darkness", "Deep Darkness - Present Truffle", 0xEB00CC),
|
||||
LocationData("Deep Darkness Darkness", "Deep Darkness - Village Truffle", 0xEB00CD),
|
||||
LocationData("Deep Darkness Darkness", "Deep Darkness - Entrance Truffle", 0xEB00CE),
|
||||
LocationData("Deep Darkness Darkness", "Deep Darkness - Barf Character", 0xEB00CF),
|
||||
LocationData("Tenda Village", "Tenda Village - Trashcan", 0xEB00D0),
|
||||
LocationData("Tenda Village", "Tenda Village - Tenda Tea", 0xEB00D1),
|
||||
LocationData("Tenda Village", "Tenda Village - Tenda Gift", 0xEB00D2),
|
||||
LocationData("Tenda Village", "Tenda Village - Tenda Gift #2", 0xEB00D3),
|
||||
LocationData("Lumine Hall", "Lumine Hall - B1F Non-Talkative Rock Present", 0xEB00D4),
|
||||
LocationData("Lumine Hall", "Lumine Hall - 1F North Path Present", 0xEB00D5),
|
||||
LocationData("Lumine Hall", "Lumine Hall - B1F Thankful Rock Corner Present", 0xEB00D6),
|
||||
LocationData("Lumine Hall", "Lumine Hall - B1F Thankful Rock Junction Present", 0xEB00D7),
|
||||
LocationData("Lumine Hall", "Lumine Hall - 1F Above Belly Button Present", 0xEB00D8),
|
||||
LocationData("Lumine Hall", "Lumine Hall - B1F Belly Button Present", 0xEB00D9),
|
||||
LocationData("Lumine Hall", "Lumine Hall - 1F Near Exit Present", 0xEB00DA),
|
||||
LocationData("Lumine Hall", "Lumine Hall - 1F Dead End Present", 0xEB00DB),
|
||||
LocationData("Lumine Hall", "Lumine Hall - B1F West Alcove Present", 0xEB00DC),
|
||||
LocationData("Lost Underworld", "Lost Underworld - Talking Rock", 0xEB00DD),
|
||||
LocationData("Lost Underworld", "Lost Underworld - East Present", 0xEB00DE),
|
||||
LocationData("Lost Underworld", "Lost Underworld - Northeast Present", 0xEB00DF),
|
||||
LocationData("Lost Underworld", "Lost Underworld - Northeast of Tenda Tribe Present", 0xEB00E0),
|
||||
LocationData("Lost Underworld", "Lost Underworld - Southwest of Tenda Tribe Present", 0xEB00E1),
|
||||
LocationData("Lost Underworld", "Lost Underworld - Evacuation Present", 0xEB00E2),
|
||||
LocationData("Fire Spring", "Fire Spring - 1st Cave Present", 0xEB00E3),
|
||||
LocationData("Fire Spring", "Fire Spring - East Corner Present", 0xEB00E4),
|
||||
LocationData("Fire Spring", "Fire Spring - Volcano Present", 0xEB00E5),
|
||||
LocationData("Fire Spring", "Fire Spring - Lone Cave Present", 0xEB00E6),
|
||||
LocationData("Fire Spring", "Fire Spring - Upper Volcano Present", 0xEB00E7),
|
||||
LocationData("Cave of the Present", "Cave of the Present - Star Master", 0xEB00EE),
|
||||
LocationData("Cave of the Present", "Cave of the Present - Broken Phase Distorter", 0xEB00EF),
|
||||
|
||||
LocationData("Happy-Happy HQ", "Carpainter Defeated", None),
|
||||
LocationData("Belch's Factory", "Belch Defeated", None),
|
||||
LocationData("Dungeon Man", "Dungeon Man Submarine", None),
|
||||
LocationData("Giant Step", "Giant Step Sanctuary", None),
|
||||
LocationData("Lilliput Steps", "Lilliput Steps Sanctuary", None),
|
||||
LocationData("Milky Well", "Milky Well Sanctuary", None),
|
||||
LocationData("Rainy Circle", "Rainy Circle Sanctuary", None),
|
||||
LocationData("Magnet Hill", "Magnet Hill Sanctuary", None),
|
||||
LocationData("Pink Cloud", "Pink Cloud Sanctuary", None),
|
||||
LocationData("Lumine Hall", "Lumine Hall Sanctuary", None),
|
||||
LocationData("Fire Spring", "Fire Spring Sanctuary", None),
|
||||
LocationData("Ness's Mind", "Sanctuary Goal", None),
|
||||
LocationData("Global ATM Access", "Any ATM", None)
|
||||
]
|
||||
|
||||
if world.options.giygas_required:
|
||||
location_table += [
|
||||
LocationData("Cave of the Past", "Cave of the Past - Present", 0xEB00F0),
|
||||
LocationData("Endgame", "Giygas", None),
|
||||
]
|
||||
|
||||
if world.options.alternate_sanctuary_goal:
|
||||
location_table += [
|
||||
LocationData("Ness's Mind", "+2 Sanctuaries", None)
|
||||
]
|
||||
|
||||
if world.options.magicant_mode in range(1, 3):
|
||||
location_table += [
|
||||
LocationData("Sea of Eden", "Magicant - Ness's Nightmare", None),
|
||||
]
|
||||
|
||||
if not world.options.magicant_mode:
|
||||
location_table += [
|
||||
LocationData("Sea of Eden", "Magicant - Ness's Nightmare", 0xEB00ED),
|
||||
]
|
||||
|
||||
if world.options.magicant_mode < MagicantMode.option_alternate_goal:
|
||||
location_table += [
|
||||
LocationData("Magicant", "Magicant - Ness's Gift", 0xEB00E8),
|
||||
LocationData("Magicant", "Magicant - Present Near Ness", 0xEB00E9),
|
||||
LocationData("Magicant", "Magicant - Lonely Present", 0xEB00EA),
|
||||
LocationData("Magicant", "Magicant - North Present", 0xEB00EB),
|
||||
LocationData("Magicant", "Magicant - Hills Present", 0xEB00EC),
|
||||
LocationData("Magicant", "Magicant - Town Present", 0xEB00FA)
|
||||
]
|
||||
if world.options.magicant_mode == MagicantMode.option_alternate_goal:
|
||||
location_table += [
|
||||
LocationData("Ness's Mind", "+1 Sanctuary", None)
|
||||
]
|
||||
|
||||
if world.options.shop_randomizer == ShopRandomizer.option_shopsanity:
|
||||
location_table += [
|
||||
LocationData("Onett", "Onett Drugstore - Right Counter Slot 1", 0xeb1000),
|
||||
LocationData("Onett", "Onett Drugstore - Right Counter Slot 2", 0xeb1001),
|
||||
LocationData("Onett", "Onett Drugstore - Right Counter Slot 3", 0xeb1002),
|
||||
LocationData("Onett", "Onett Drugstore - Right Counter Slot 4", 0xeb1003),
|
||||
LocationData("Onett", "Onett Drugstore - Right Counter Slot 5", 0xeb1004),
|
||||
LocationData("Onett", "Onett Drugstore - Left Counter", 0xeb1007),
|
||||
LocationData("Summers", "Summers - Beach Cart", 0xeb100e),
|
||||
LocationData("Onett", "Onett Burger Shop - Slot 1", 0xeb1015),
|
||||
LocationData("Onett", "Onett Burger Shop - Slot 2", 0xeb1016),
|
||||
LocationData("Onett", "Onett Burger Shop - Slot 3", 0xeb1017),
|
||||
LocationData("Onett", "Onett Burger Shop - Slot 4", 0xeb1018),
|
||||
LocationData("Onett", "Onett Bakery - Slot 1", 0xeb101c),
|
||||
LocationData("Onett", "Onett Bakery - Slot 2", 0xeb101d),
|
||||
LocationData("Onett", "Onett Bakery - Slot 3", 0xeb101e),
|
||||
LocationData("Onett", "Onett Bakery - Slot 4", 0xeb101f),
|
||||
LocationData("Twoson", "Twoson Department Store Burger Shop - Slot 1", 0xeb1023),
|
||||
LocationData("Twoson", "Twoson Department Store Burger Shop - Slot 2", 0xeb1024),
|
||||
LocationData("Twoson", "Twoson Department Store Burger Shop - Slot 3", 0xeb1025),
|
||||
LocationData("Twoson", "Twoson Department Store Burger Shop - Slot 4", 0xeb1026),
|
||||
LocationData("Twoson", "Twoson Department Store Bakery - Slot 1", 0xeb102a),
|
||||
LocationData("Twoson", "Twoson Department Store Bakery - Slot 2", 0xeb102b),
|
||||
LocationData("Twoson", "Twoson Department Store Bakery - Slot 3", 0xeb102c),
|
||||
LocationData("Twoson", "Twoson Department Store Bakery - Slot 4", 0xeb102d),
|
||||
LocationData("Twoson", "Twoson Department Store Top Floor - Right Counter Slot 1", 0xeb1031),
|
||||
LocationData("Twoson", "Twoson Department Store Top Floor - Right Counter Slot 2", 0xeb1032),
|
||||
LocationData("Twoson", "Twoson Department Store Top Floor - Right Counter Slot 3", 0xeb1033),
|
||||
LocationData("Twoson", "Twoson Department Store Top Floor - Right Counter Slot 4", 0xeb1034),
|
||||
LocationData("Twoson", "Twoson Department Store Top Floor - Right Counter Slot 5", 0xeb1035),
|
||||
LocationData("Twoson", "Twoson Department Store Top Floor - Right Counter Slot 6", 0xeb1036),
|
||||
LocationData("Twoson", "Twoson Department Store Top Floor - Left Counter Slot 1", 0xeb1038),
|
||||
LocationData("Twoson", "Twoson Department Store Top Floor - Left Counter Slot 2", 0xeb1039),
|
||||
LocationData("Summers", "Summers - Magic Cake Cart Shop Slot", 0xeb103f),
|
||||
LocationData("Twoson", "Burglin Park Junk Shop - Slot 1", 0xeb1046),
|
||||
LocationData("Twoson", "Burglin Park Junk Shop - Slot 2", 0xeb1047),
|
||||
LocationData("Twoson", "Burglin Park Junk Shop - Slot 3", 0xeb1048),
|
||||
LocationData("Twoson", "Burglin Park Junk Shop - Slot 4", 0xeb1049),
|
||||
LocationData("Twoson", "Burglin Park Junk Shop - Slot 5", 0xeb104a),
|
||||
LocationData("Twoson", "Burglin Park Junk Shop - Slot 6", 0xeb104b),
|
||||
LocationData("Twoson", "Burglin Park Bread Stand - Slot 1", 0xeb105b),
|
||||
LocationData("Twoson", "Burglin Park Bread Stand - Slot 2", 0xeb105c),
|
||||
LocationData("Twoson", "Burglin Park Bread Stand - Slot 3", 0xeb105d),
|
||||
LocationData("Twoson", "Burglin Park Bread Stand - Slot 4", 0xeb105e),
|
||||
LocationData("Twoson", "Burglin Park Bread Stand - Slot 5", 0xeb105f),
|
||||
LocationData("Twoson", "Burglin Park Bread Stand - Slot 6", 0xeb1060),
|
||||
LocationData("Twoson", "Burglin Park - Banana Stand", 0xeb1062),
|
||||
LocationData("Happy-Happy Village", "Happy-Happy Village Drugstore - Right Counter Slot 1", 0xeb1069),
|
||||
LocationData("Happy-Happy Village", "Happy-Happy Village Drugstore - Right Counter Slot 2", 0xeb106a),
|
||||
LocationData("Happy-Happy Village", "Happy-Happy Village Drugstore - Right Counter Slot 3", 0xeb106b),
|
||||
LocationData("Happy-Happy Village", "Happy-Happy Village Drugstore - Right Counter Slot 4", 0xeb106c),
|
||||
LocationData("Happy-Happy Village", "Happy-Happy Village Drugstore - Right Counter Slot 5", 0xeb106d),
|
||||
LocationData("Threed", "Threed Drugstore - Right Counter Slot 1", 0xeb1070),
|
||||
LocationData("Threed", "Threed Drugstore - Right Counter Slot 2", 0xeb1071),
|
||||
LocationData("Threed", "Threed Drugstore - Right Counter Slot 3", 0xeb1072),
|
||||
LocationData("Threed", "Threed Drugstore - Right Counter Slot 4", 0xeb1073),
|
||||
LocationData("Threed", "Threed Drugstore - Right Counter Slot 5", 0xeb1074),
|
||||
LocationData("Threed", "Threed Drugstore - Left Counter Slot 1", 0xeb1077),
|
||||
LocationData("Threed", "Threed Drugstore - Left Counter Slot 2", 0xeb1078),
|
||||
LocationData("Threed", "Threed Drugstore - Left Counter Slot 3", 0xeb1079),
|
||||
LocationData("Threed", "Threed Drugstore - Left Counter Slot 4", 0xeb107a),
|
||||
LocationData("Threed", "Threed Drugstore - Left Counter Slot 5", 0xeb107b),
|
||||
LocationData("Threed", "Threed - Arms Dealer Slot 1", 0xeb107e),
|
||||
LocationData("Threed", "Threed - Arms Dealer Slot 2", 0xeb107f),
|
||||
LocationData("Threed", "Threed - Arms Dealer Slot 3", 0xeb1080),
|
||||
LocationData("Threed", "Threed - Arms Dealer Slot 4", 0xeb1081),
|
||||
LocationData("Threed", "Threed Bakery - Slot 1", 0xeb1085),
|
||||
LocationData("Threed", "Threed Bakery - Slot 2", 0xeb1086),
|
||||
LocationData("Threed", "Threed Bakery - Slot 3", 0xeb1087),
|
||||
LocationData("Threed", "Threed Bakery - Slot 4", 0xeb1088),
|
||||
LocationData("Threed", "Threed Bakery - Slot 5", 0xeb1089),
|
||||
LocationData("Threed", "Threed Bakery - Slot 6", 0xeb108a),
|
||||
LocationData("Threed", "Threed Bakery - Slot 7", 0xeb108b),
|
||||
LocationData("Scaraba", "Scaraba - Expensive Water Guy", 0xeb108c),
|
||||
LocationData("Winters", "Winters Drugstore - Slot 1", 0xeb1093),
|
||||
LocationData("Winters", "Winters Drugstore - Slot 2", 0xeb1094),
|
||||
LocationData("Winters", "Winters Drugstore - Slot 3", 0xeb1095),
|
||||
LocationData("Winters", "Winters Drugstore - Slot 4", 0xeb1096),
|
||||
LocationData("Winters", "Winters Drugstore - Slot 5", 0xeb1097),
|
||||
LocationData("Winters", "Winters Drugstore - Slot 6", 0xeb1098),
|
||||
LocationData("Winters", "Winters Drugstore - Slot 7", 0xeb1099),
|
||||
LocationData("Saturn Valley", "Saturn Valley Shop - Center Saturn Slot 1", 0xeb109a),
|
||||
LocationData("Saturn Valley", "Saturn Valley Shop - Center Saturn Slot 2", 0xeb109b),
|
||||
LocationData("Saturn Valley", "Saturn Valley Shop - Center Saturn Slot 3", 0xeb109c),
|
||||
LocationData("Saturn Valley", "Saturn Valley Shop - Center Saturn Slot 4", 0xeb109d),
|
||||
LocationData("Saturn Valley", "Saturn Valley Shop - Center Saturn Slot 5", 0xeb109e),
|
||||
LocationData("Dusty Dunes Desert", "Dusty Dunes Drugstore - Counter Slot 1", 0xeb10a1),
|
||||
LocationData("Dusty Dunes Desert", "Dusty Dunes Drugstore - Counter Slot 2", 0xeb10a2),
|
||||
LocationData("Dusty Dunes Desert", "Dusty Dunes Drugstore - Counter Slot 3", 0xeb10a3),
|
||||
LocationData("Dusty Dunes Desert", "Dusty Dunes Drugstore - Counter Slot 4", 0xeb10a4),
|
||||
LocationData("Dusty Dunes Desert", "Dusty Dunes Drugstore - Counter Slot 5", 0xeb10a5),
|
||||
LocationData("Dusty Dunes Desert", "Dusty Dunes - Arms Dealer Slot 1", 0xeb10a8),
|
||||
LocationData("Dusty Dunes Desert", "Dusty Dunes - Arms Dealer Slot 2", 0xeb10a9),
|
||||
LocationData("Dusty Dunes Desert", "Dusty Dunes - Arms Dealer Slot 3", 0xeb10aa),
|
||||
LocationData("Dusty Dunes Desert", "Dusty Dunes - Arms Dealer Slot 4", 0xeb10ab),
|
||||
LocationData("Fourside", "Fourside Bakery - Slot 1", 0xeb10af),
|
||||
LocationData("Fourside", "Fourside Bakery - Slot 2", 0xeb10b0),
|
||||
LocationData("Fourside", "Fourside Bakery - Slot 3", 0xeb10b1),
|
||||
LocationData("Fourside", "Fourside Bakery - Slot 4", 0xeb10b2),
|
||||
LocationData("Fourside", "Fourside Bakery - Slot 5", 0xeb10b3),
|
||||
LocationData("Fourside", "Fourside Bakery - Slot 6", 0xeb10b4),
|
||||
LocationData("Fourside", "Fourside Department Store - Tool Shop Slot 1", 0xeb10b6),
|
||||
LocationData("Fourside", "Fourside Department Store - Tool Shop Slot 2", 0xeb10b7),
|
||||
LocationData("Fourside", "Fourside Department Store - Tool Shop Slot 3", 0xeb10b8),
|
||||
LocationData("Fourside", "Fourside Department Store - Tool Shop Slot 4", 0xeb10b9),
|
||||
LocationData("Fourside", "Fourside Department Store - Tool Shop Slot 5", 0xeb10ba),
|
||||
LocationData("Fourside", "Fourside Department Store - Tool Shop Slot 6", 0xeb10bb),
|
||||
LocationData("Fourside", "Fourside Department Store - Tool Shop Slot 7", 0xeb10bc),
|
||||
LocationData("Fourside", "Fourside Department Store - Shop Shop Slot 1", 0xeb10bd),
|
||||
LocationData("Fourside", "Fourside Department Store - Shop Shop Slot 2", 0xeb10be),
|
||||
LocationData("Fourside", "Fourside Department Store - Shop Shop Slot 3", 0xeb10bf),
|
||||
LocationData("Fourside", "Fourside Department Store - Shop Shop Slot 4", 0xeb10c0),
|
||||
LocationData("Fourside", "Fourside Department Store - Food Shop Slot 1", 0xeb10c4),
|
||||
LocationData("Fourside", "Fourside Department Store - Food Shop Slot 2", 0xeb10c5),
|
||||
LocationData("Fourside", "Fourside Department Store - Food Shop Slot 3", 0xeb10c6),
|
||||
LocationData("Fourside", "Fourside Department Store - Food Shop Slot 4", 0xeb10c7),
|
||||
LocationData("Fourside", "Fourside Department Store - Food Shop Slot 5", 0xeb10c8),
|
||||
LocationData("Fourside", "Fourside Department Store - 2F Cart Slot 1", 0xeb10cb),
|
||||
LocationData("Fourside", "Fourside Department Store - 2F Cart Slot 2", 0xeb10cc),
|
||||
LocationData("Fourside", "Fourside Department Store - 2F Cart Slot 3", 0xeb10cd),
|
||||
LocationData("Fourside", "Fourside Department Store - 2F Cart Slot 4", 0xeb10ce),
|
||||
LocationData("Fourside", "Fourside Department Store - 2F Cart Slot 5", 0xeb10cf),
|
||||
LocationData("Fourside", "Fourside Department Store - 2F Cart Slot 6", 0xeb10d0),
|
||||
LocationData("Fourside", "Fourside Department Store - 2F Cart Slot 7", 0xeb10d1),
|
||||
LocationData("Fourside", "Fourside Department Store - Toys Shop Slot 1", 0xeb10d2),
|
||||
LocationData("Fourside", "Fourside Department Store - Toys Shop Slot 2", 0xeb10d3),
|
||||
LocationData("Fourside", "Fourside Department Store - Toys Shop Slot 3", 0xeb10d4),
|
||||
LocationData("Fourside", "Fourside Department Store - Toys Shop Slot 4", 0xeb10d5),
|
||||
LocationData("Fourside", "Fourside Department Store - Toys Shop Slot 5", 0xeb10d6),
|
||||
LocationData("Fourside", "Fourside Department Store - Toys Shop Slot 6", 0xeb10d7),
|
||||
LocationData("Fourside", "Fourside Department Store - Sports Shop Slot 1", 0xeb10d9),
|
||||
LocationData("Fourside", "Fourside Department Store - Sports Shop Slot 2", 0xeb10da),
|
||||
LocationData("Fourside", "Fourside Department Store - Sports Shop Slot 3", 0xeb10db),
|
||||
LocationData("Fourside", "Fourside Department Store - Sports Shop Slot 4", 0xeb10dc),
|
||||
LocationData("Fourside", "Fourside Department Store - Burger Shop Slot 1", 0xeb10e0),
|
||||
LocationData("Fourside", "Fourside Department Store - Burger Shop Slot 2", 0xeb10e1),
|
||||
LocationData("Fourside", "Fourside Department Store - Burger Shop Slot 3", 0xeb10e2),
|
||||
LocationData("Fourside", "Fourside Department Store - Burger Shop Slot 4", 0xeb10e3),
|
||||
LocationData("Fourside", "Fourside Department Store - Burger Shop Slot 5", 0xeb10e4),
|
||||
LocationData("Fourside", "Fourside Department Store - Arms Dealer Slot 1", 0xeb10e7),
|
||||
LocationData("Fourside", "Fourside Department Store - Arms Dealer Slot 2", 0xeb10e8),
|
||||
LocationData("Fourside", "Fourside Department Store - Arms Dealer Slot 3", 0xeb10e9),
|
||||
LocationData("Fourside", "Fourside Department Store - Arms Dealer Slot 4", 0xeb10ea),
|
||||
LocationData("Fourside", "Fourside Department Store - Arms Dealer Slot 5", 0xeb10eb),
|
||||
LocationData("Fourside", "Fourside - Northeast Alley Junk Shop Slot 1", 0xeb10ee),
|
||||
LocationData("Fourside", "Fourside - Northeast Alley Junk Shop Slot 2", 0xeb10ef),
|
||||
LocationData("Fourside", "Fourside - Northeast Alley Junk Shop Slot 3", 0xeb10f0),
|
||||
LocationData("Fourside", "Fourside - Northeast Alley Junk Shop Slot 4", 0xeb10f1),
|
||||
LocationData("Summers", "Summers - Scam Shop Slot 1", 0xeb1103),
|
||||
LocationData("Summers", "Summers - Scam Shop Slot 2", 0xeb1104),
|
||||
LocationData("Summers", "Summers - Scam Shop Slot 3", 0xeb1105),
|
||||
LocationData("Summers", "Summers - Scam Shop Slot 4", 0xeb1106),
|
||||
LocationData("Summers", "Summers - Scam Shop Slot 5", 0xeb1107),
|
||||
LocationData("Summers", "Summers - Scam Shop Slot 6", 0xeb1108),
|
||||
LocationData("Summers", "Summers - Scam Shop Slot 7", 0xeb1109),
|
||||
LocationData("Summers", "Summers Harbor - Shop Slot 1", 0xeb110a),
|
||||
LocationData("Summers", "Summers Harbor - Shop Slot 2", 0xeb110b),
|
||||
LocationData("Summers", "Summers Harbor - Shop Slot 3", 0xeb110c),
|
||||
LocationData("Summers", "Summers Harbor - Shop Slot 4", 0xeb110d),
|
||||
LocationData("Summers", "Summers Harbor - Shop Slot 5", 0xeb110e),
|
||||
LocationData("Summers", "Summers Harbor - Shop Slot 6", 0xeb110f),
|
||||
LocationData("Summers", "Summers Harbor - Shop Slot 7", 0xeb1110),
|
||||
LocationData("Summers", "Summers Restaurant - Slot 1", 0xeb1111),
|
||||
LocationData("Summers", "Summers Restaurant - Slot 2", 0xeb1112),
|
||||
LocationData("Summers", "Summers Restaurant - Slot 3", 0xeb1113),
|
||||
LocationData("Summers", "Summers Restaurant - Slot 4", 0xeb1114),
|
||||
LocationData("Summers", "Summers Restaurant - Slot 5", 0xeb1115),
|
||||
LocationData("Summers", "Summers Restaurant - Slot 6", 0xeb1116),
|
||||
LocationData("Scaraba", "Scaraba - Indoors Shop Slot 1", 0xeb1118),
|
||||
LocationData("Scaraba", "Scaraba - Indoors Shop Slot 2", 0xeb1119),
|
||||
LocationData("Scaraba", "Scaraba - Indoors Shop Slot 3", 0xeb111a),
|
||||
LocationData("Scaraba", "Scaraba - Indoors Shop Slot 4", 0xeb111b),
|
||||
LocationData("Scaraba", "Scaraba - Indoors Shop Slot 5", 0xeb111c),
|
||||
LocationData("Scaraba", "Scaraba - Indoors Shop Slot 6", 0xeb111d),
|
||||
LocationData("Scaraba", "Scaraba Bazaar - Red Snake Carpet Slot 1", 0xeb1126),
|
||||
LocationData("Scaraba", "Scaraba Bazaar - Red Snake Carpet Slot 2", 0xeb1127),
|
||||
LocationData("Scaraba", "Scaraba Bazaar - Red Snake Carpet Slot 3", 0xeb1128),
|
||||
LocationData("Scaraba", "Scaraba Bazaar - Bottom Left Carpet Slot 1", 0xeb112d),
|
||||
LocationData("Scaraba", "Scaraba Bazaar - Bottom Left Carpet Slot 2", 0xeb112e),
|
||||
LocationData("Scaraba", "Scaraba Bazaar - Bottom Left Carpet Slot 3", 0xeb112f),
|
||||
LocationData("Scaraba", "Scaraba Bazaar - Bottom Left Carpet Slot 4", 0xeb1130),
|
||||
LocationData("Scaraba", "Scaraba Bazaar - Bottom Left Carpet Slot 5", 0xeb1131),
|
||||
LocationData("Scaraba", "Scaraba Bazaar - Bottom Left Carpet Slot 6", 0xeb1132),
|
||||
LocationData("Scaraba", "Scaraba Hotel - Arms Dealer Slot 1", 0xeb1134),
|
||||
LocationData("Scaraba", "Scaraba Hotel - Arms Dealer Slot 2", 0xeb1135),
|
||||
LocationData("Scaraba", "Scaraba Hotel - Arms Dealer Slot 3", 0xeb1136),
|
||||
LocationData("Scaraba", "Scaraba Hotel - Arms Dealer Slot 4", 0xeb1137),
|
||||
LocationData("Deep Darkness", "Deep Darkness - Businessman Slot 1", 0xeb113b),
|
||||
LocationData("Deep Darkness", "Deep Darkness - Businessman Slot 2", 0xeb113c),
|
||||
LocationData("Deep Darkness", "Deep Darkness - Businessman Slot 3", 0xeb113d),
|
||||
LocationData("Deep Darkness", "Deep Darkness - Businessman Slot 4", 0xeb113e),
|
||||
LocationData("Deep Darkness", "Deep Darkness - Businessman Slot 5", 0xeb113f),
|
||||
LocationData("Deep Darkness", "Deep Darkness - Businessman Slot 6", 0xeb1140),
|
||||
LocationData("Deep Darkness", "Deep Darkness - Businessman Slot 7", 0xeb1141),
|
||||
LocationData("Saturn Valley", "Saturn Valley Shop - Post-Belch Saturn Slot 1", 0xeb1157),
|
||||
LocationData("Saturn Valley", "Saturn Valley Shop - Post-Belch Saturn Slot 2", 0xeb1158),
|
||||
LocationData("Saturn Valley", "Saturn Valley Shop - Post-Belch Saturn Slot 3", 0xeb1159),
|
||||
LocationData("Saturn Valley", "Saturn Valley Shop - Post-Belch Saturn Slot 4", 0xeb115a),
|
||||
LocationData("Southern Scaraba", "Scaraba - Southern Camel Shop Slot 1", 0xeb115e),
|
||||
LocationData("Southern Scaraba", "Scaraba - Southern Camel Shop Slot 2", 0xeb115f),
|
||||
LocationData("Southern Scaraba", "Scaraba - Southern Camel Shop Slot 3", 0xeb1160),
|
||||
LocationData("Southern Scaraba", "Scaraba - Southern Camel Shop Slot 4", 0xeb1161),
|
||||
LocationData("Southern Scaraba", "Scaraba - Southern Camel Shop Slot 5", 0xeb1162),
|
||||
LocationData("Southern Scaraba", "Scaraba - Southern Camel Shop Slot 6", 0xeb1163),
|
||||
LocationData("Southern Scaraba", "Scaraba - Southern Camel Shop Slot 7", 0xeb1164),
|
||||
LocationData("Deep Darkness", "Deep Darkness - Arms Dealer Slot 1", 0xeb1165),
|
||||
LocationData("Deep Darkness", "Deep Darkness - Arms Dealer Slot 2", 0xeb1166),
|
||||
LocationData("Deep Darkness", "Deep Darkness - Arms Dealer Slot 3", 0xeb1167),
|
||||
LocationData("Deep Darkness", "Deep Darkness - Arms Dealer Slot 4", 0xeb1168),
|
||||
LocationData("Lost Underworld", "Lost Underworld - Tenda Camp Shop Slot 1", 0xeb116c),
|
||||
LocationData("Lost Underworld", "Lost Underworld - Tenda Camp Shop Slot 2", 0xeb116d),
|
||||
LocationData("Lost Underworld", "Lost Underworld - Tenda Camp Shop Slot 3", 0xeb116e),
|
||||
LocationData("Lost Underworld", "Lost Underworld - Tenda Camp Shop Slot 4", 0xeb116f),
|
||||
LocationData("Lost Underworld", "Lost Underworld - Tenda Camp Shop Slot 5", 0xeb1170),
|
||||
LocationData("Lost Underworld", "Lost Underworld - Tenda Camp Shop Slot 6", 0xeb1171),
|
||||
LocationData("Lost Underworld", "Lost Underworld - Tenda Camp Shop Slot 7", 0xeb1172),
|
||||
LocationData("Happy-Happy Village", "Happy-Happy Village Drugstore - Left Counter Slot 1", 0xeb117a),
|
||||
LocationData("Happy-Happy Village", "Happy-Happy Village Drugstore - Left Counter Slot 2", 0xeb117b),
|
||||
LocationData("Happy-Happy Village", "Happy-Happy Village Drugstore - Left Counter Slot 3", 0xeb117c),
|
||||
LocationData("Happy-Happy Village", "Happy-Happy Village Drugstore - Left Counter Slot 4", 0xeb117d),
|
||||
LocationData("Happy-Happy Village", "Happy-Happy Village Drugstore - Left Counter Slot 5", 0xeb117e),
|
||||
LocationData("Happy-Happy Village", "Happy-Happy Village Drugstore - Left Counter Slot 6", 0xeb117f),
|
||||
LocationData("Happy-Happy Village", "Happy-Happy Village Drugstore - Left Counter Slot 7", 0xeb1180),
|
||||
LocationData("Grapefruit Falls", "Grapefruit Falls - Hiker Shop Slot 1", 0xeb1181),
|
||||
LocationData("Grapefruit Falls", "Grapefruit Falls - Hiker Shop Slot 2", 0xeb1182),
|
||||
LocationData("Grapefruit Falls", "Grapefruit Falls - Hiker Shop Slot 3", 0xeb1183),
|
||||
LocationData("Saturn Valley", "Saturn Valley Shop - Top Saturn Slot 1", 0xeb1188),
|
||||
LocationData("Saturn Valley", "Saturn Valley Shop - Top Saturn Slot 2", 0xeb1189),
|
||||
LocationData("Saturn Valley", "Saturn Valley Shop - Top Saturn Slot 3", 0xeb118a),
|
||||
LocationData("Saturn Valley", "Saturn Valley Shop - Top Saturn Slot 4", 0xeb118b),
|
||||
LocationData("Saturn Valley", "Saturn Valley Shop - Top Saturn Slot 5", 0xeb118c),
|
||||
LocationData("Saturn Valley", "Saturn Valley Shop - Top Saturn Slot 6", 0xeb118d),
|
||||
LocationData("Saturn Valley", "Saturn Valley Shop - Top Saturn Slot 7", 0xeb118e),
|
||||
LocationData("Dusty Dunes Desert", "Dusty Dunes Drugstore - Left Shop Slot 1", 0xeb118f),
|
||||
LocationData("Dusty Dunes Desert", "Dusty Dunes Drugstore - Left Shop Slot 2", 0xeb1190),
|
||||
LocationData("Dusty Dunes Desert", "Dusty Dunes Drugstore - Left Shop Slot 3", 0xeb1191),
|
||||
LocationData("Dusty Dunes Desert", "Dusty Dunes Drugstore - Left Shop Slot 4", 0xeb1192),
|
||||
LocationData("Dusty Dunes Desert", "Dusty Dunes Drugstore - Left Shop Slot 5", 0xeb1193),
|
||||
LocationData("Dusty Dunes Desert", "Dusty Dunes Drugstore - Left Shop Slot 6", 0xeb1194),
|
||||
LocationData("Dusty Dunes Desert", "Dusty Dunes Drugstore - Left Shop Slot 7", 0xeb1195),
|
||||
LocationData("Dusty Dunes Desert", "Dusty Dunes - Mine Food Cart Slot 1", 0xeb1196),
|
||||
LocationData("Dusty Dunes Desert", "Dusty Dunes - Mine Food Cart Slot 2", 0xeb1197),
|
||||
LocationData("Dusty Dunes Desert", "Dusty Dunes - Mine Food Cart Slot 3", 0xeb1198),
|
||||
LocationData("Dusty Dunes Desert", "Dusty Dunes - Mine Food Cart Slot 4", 0xeb1199),
|
||||
LocationData("Dusty Dunes Desert", "Dusty Dunes - Mine Food Cart Slot 5", 0xeb119a),
|
||||
LocationData("Dusty Dunes Desert", "Dusty Dunes - Mine Food Cart Slot 6", 0xeb119b),
|
||||
LocationData("Dusty Dunes Desert", "Dusty Dunes - Mine Food Cart Slot 7", 0xeb119c),
|
||||
LocationData("Moonside", "Moonside Hotel - Shop Slot 1", 0xeb119d),
|
||||
LocationData("Moonside", "Moonside Hotel - Shop Slot 2", 0xeb119e),
|
||||
LocationData("Moonside", "Moonside Hotel - Shop Slot 3", 0xeb119f),
|
||||
LocationData("Moonside", "Moonside Hotel - Shop Slot 4", 0xeb11a0),
|
||||
LocationData("Moonside", "Moonside Hotel - Shop Slot 5", 0xeb11a1),
|
||||
LocationData("Dalaam", "Dalaam Restaurant - Slot 1", 0xeb11a4),
|
||||
LocationData("Dalaam", "Dalaam Restaurant - Slot 2", 0xeb11a5),
|
||||
LocationData("Dalaam", "Dalaam Restaurant - Slot 3", 0xeb11a6),
|
||||
LocationData("Dalaam", "Dalaam Restaurant - Slot 4", 0xeb11a7),
|
||||
LocationData("Scaraba", "Scaraba Bazaar - Delicacy Shop Slot 1", 0xeb11ab),
|
||||
LocationData("Scaraba", "Scaraba Bazaar - Delicacy Shop Slot 2", 0xeb11ac),
|
||||
LocationData("Scaraba", "Scaraba Bazaar - Delicacy Shop Slot 3", 0xeb11ad),
|
||||
LocationData("Scaraba", "Scaraba Bazaar - Delicacy Shop Slot 4", 0xeb11ae),
|
||||
LocationData("Scaraba", "Scaraba Bazaar - Delicacy Shop Slot 5", 0xeb11af),
|
||||
LocationData("Scaraba", "Scaraba Bazaar - Delicacy Shop Slot 6", 0xeb11b0),
|
||||
LocationData("Scaraba", "Scaraba Bazaar - Delicacy Shop Slot 7", 0xeb11b1),
|
||||
LocationData("Common Condiment Shop", "Twoson/Scaraba - Shared Condiment Shop Slot 1", 0xeb11b2),
|
||||
LocationData("Common Condiment Shop", "Twoson/Scaraba - Shared Condiment Shop Slot 2", 0xeb11b3),
|
||||
LocationData("Common Condiment Shop", "Twoson/Scaraba - Shared Condiment Shop Slot 3", 0xeb11b4),
|
||||
LocationData("Common Condiment Shop", "Twoson/Scaraba - Shared Condiment Shop Slot 4", 0xeb11b5),
|
||||
LocationData("Common Condiment Shop", "Twoson/Scaraba - Shared Condiment Shop Slot 5", 0xeb11b6),
|
||||
LocationData("Common Condiment Shop", "Twoson/Scaraba - Shared Condiment Shop Slot 6", 0xeb11b7),
|
||||
LocationData("Common Condiment Shop", "Twoson/Scaraba - Shared Condiment Shop Slot 7", 0xeb11b8),
|
||||
LocationData("Andonuts Lab Area", "Andonuts Lab - Caveman Shop Slot 1", 0xeb11c0),
|
||||
LocationData("Andonuts Lab Area", "Andonuts Lab - Caveman Shop Slot 2", 0xeb11c1),
|
||||
LocationData("Andonuts Lab Area", "Andonuts Lab - Caveman Shop Slot 3", 0xeb11c2),
|
||||
LocationData("Andonuts Lab Area", "Andonuts Lab - Caveman Shop Slot 4", 0xeb11c3),
|
||||
LocationData("Andonuts Lab Area", "Andonuts Lab - Caveman Shop Slot 5", 0xeb11c4)
|
||||
]
|
||||
|
||||
if world.options.magicant_mode < MagicantMode.option_alternate_goal:
|
||||
location_table += [
|
||||
LocationData("Magicant", "Magicant - Shop Slot 1", 0xeb10f5),
|
||||
LocationData("Magicant", "Magicant - Shop Slot 2", 0xeb10f6)
|
||||
]
|
||||
|
||||
return location_table
|
||||
625
worlds/earthbound/Options.py
Normal file
625
worlds/earthbound/Options.py
Normal file
@@ -0,0 +1,625 @@
|
||||
from dataclasses import dataclass
|
||||
from Options import (Toggle, DefaultOnToggle, DeathLink, Choice, Range, PerGameCommonOptions, StartInventoryPool,
|
||||
OptionGroup, FreeText, Visibility, PlandoBosses)
|
||||
from .modules.boss_shuffle import boss_plando_keys
|
||||
|
||||
|
||||
class GiygasRequired(DefaultOnToggle):
|
||||
"""If enabled, your goal will be to defeat Giygas at the Cave of the Past.
|
||||
If disabled, your goal will either complete automatically upon completing
|
||||
enough Sanctuaries, or completing Magicant if it is required."""
|
||||
display_name = "Giygas Required"
|
||||
|
||||
|
||||
class SanctuariesRequired(Range):
|
||||
"""How many of the eight "Your Sanctuary" locations are required to be cleared."""
|
||||
display_name = "Required Sanctuaries"
|
||||
range_start = 1
|
||||
range_end = 8
|
||||
default = 4
|
||||
|
||||
|
||||
class SanctuaryAltGoal(Toggle):
|
||||
"""If enabled, you will be able to win by completing 2 more Sanctuaries than are required.
|
||||
Does nothing if 7 or more Sanctuaries are required, or if Magicant and Giygas are not required."""
|
||||
display_name = "Sanctuary Alternate Goal"
|
||||
|
||||
|
||||
class MagicantMode(Choice):
|
||||
"""PSI Location: You will be able to find a Magicant teleport item. Ness's Nightmare contains a PSI location, and no stat boost.
|
||||
Required: You will unlock the Magicant Teleport upon reaching your Sanctuary goal. If Giygas is required, beating Ness's Nightmare will unlock the Cave of the Past and grant a party-wide stat boost. Otherwise, Ness's Nightmare will finish your game.
|
||||
Alternate Goal: You will unlock the Magicant Teleport upon reaching one more Sanctuary than required. Beating Ness's Nightmare will finish your game. Does nothing if Giygas is not required, or if 8 Sanctuaries are required. Magicant locations are removed from the multiworld, but contain random junk for yourself.
|
||||
Optional Boost: You will be able to find a Magicant teleport item. Beating Ness's Nightmare will grant a party-wide stat boost. Magicant locations are removed from the multiworld, but contain random junk for yourself.
|
||||
Removed: Magicant will be completely inacessible."""
|
||||
display_name = "Magicant Mode"
|
||||
option_psi_location = 0
|
||||
option_required = 1
|
||||
option_alternate_goal = 2
|
||||
option_optional_boost = 3
|
||||
option_removed = 4
|
||||
default = 0
|
||||
|
||||
|
||||
class MonkeyCavesMode(Choice):
|
||||
"""Chests: Items required to finish the Monkey Caves will be forcibly placed on the chests that can be found in-between rooms of the monkey caves. The "reward" locations, usually found at the end of a branch, are still random. If you waste chest items, they will need to be replaced via the methods in hunt mode.
|
||||
Hunt: Items required to finish the Monkey Caves will needsell you every minor item needed to be found outside. They can be obtained from the Dusty Dunes drugstore, the Fourside department store, and the pizza shop in either Twoson or Threed.
|
||||
Shop: The monkey outside the Monkey Caves will sell every item needed to complete the caves and is not affected by shop randomization.
|
||||
Solved: The Monkey Caves monkeys will already be moved out of the way and not require any items."""
|
||||
display_name = "Monkey Caves Mode"
|
||||
option_chests = 0
|
||||
option_hunt = 1
|
||||
option_shop = 2
|
||||
option_solved = 3
|
||||
default = 1
|
||||
|
||||
|
||||
class ShortenPrayers(DefaultOnToggle):
|
||||
"""If enabled, the Prayer cutscenes while fighting Giygas will be skipped, excluding the final one."""
|
||||
display_name = "Skip Prayer Sequences"
|
||||
|
||||
|
||||
class RandomStartLocation(Toggle):
|
||||
"""If disabled, you will always start at Ness's house with no teleports unlocked.
|
||||
If enabled, you will start at a random teleport destination with one teleport unlocked.
|
||||
Additionally, you will need to fight Captain Strong to access the north part of Onett if this is enabled."""
|
||||
display_name = "Random Starting Location"
|
||||
|
||||
|
||||
class LocalTeleports(Toggle):
|
||||
"""Forces all teleports and Poo PSI to be placed locally in your world."""
|
||||
display_name = "Local Teleports"
|
||||
|
||||
|
||||
class CharacterShuffle(Choice):
|
||||
"""Shuffled: Characters will be shuffled amongst Character Locations. Extra locations will have Flying Man, a Teddy Bear, or a Super Plush Bear.
|
||||
Anywhere: Characters can be found anywhere in the multiworld, and character locations will have regular checks.
|
||||
See the Game Page for more information on Character Locations."""
|
||||
display_name = "Character Shuffle"
|
||||
option_shuffled = 0
|
||||
option_anywhere = 1
|
||||
default = 0
|
||||
|
||||
|
||||
class PSIShuffle(Choice):
|
||||
"""None: Characters will learn their normal PSI skills.
|
||||
Basic: Offensive and Assist PSI will be shuffled. Recovery PSI is not modified. Ness's Favorite Thing will be named Wave in other slots.
|
||||
Extended: Basic shuffle, but includes Jeff gadgets and some combat items.
|
||||
See the Game Page for more information."""
|
||||
display_name = "PSI Shuffle"
|
||||
option_none = 0
|
||||
option_basic = 1
|
||||
option_extended = 2
|
||||
|
||||
|
||||
class BossShuffle(PlandoBosses):
|
||||
"""Shuffles boss encounters amongst each other."""
|
||||
display_name = "Boss Shuffle"
|
||||
|
||||
option_false = 0
|
||||
option_true = 1
|
||||
default = 0
|
||||
bosses = boss_plando_keys
|
||||
locations = boss_plando_keys
|
||||
duplicate_bosses = False
|
||||
|
||||
@classmethod
|
||||
def can_place_boss(cls, boss: str, location: str) -> bool:
|
||||
return True
|
||||
|
||||
|
||||
class DecoupleDiamondDog(Toggle):
|
||||
"""Shuffles Diamond Dog as a boss separate from Carbon Dog. Carbon Dog will transform into a random boss.
|
||||
Does nothing if Boss Shuffle is disabled."""
|
||||
display_name = "Decouple Diamond Dog"
|
||||
|
||||
|
||||
class ShuffleGiygas(Toggle):
|
||||
"""Adds the standalone Giygas fight to the shuffled boss pool.
|
||||
This only applies to the second phase Giygas. The prayer fight is not affected.
|
||||
Does nothing if Boss Shuffle is disabled."""
|
||||
display_name = "Add Giygas to Boss Pool"
|
||||
|
||||
|
||||
class BanFlashFavorite(Toggle):
|
||||
"""If enabled, allows PSI Flash to be shuffled onto the Favorite Thing PSI slot. Can be quite annoying early-game.
|
||||
Does nothing if PSI Shuffle is set to None."""
|
||||
display_name = "Flash as Favorite"
|
||||
|
||||
|
||||
class PreFixItems(Toggle):
|
||||
"""If enabled, broken items in the multiworld pool will be replaced with their fixed versions.
|
||||
This does not affect any items that are not placed by the multiworld."""
|
||||
display_name = "Prefixed Items"
|
||||
|
||||
|
||||
class AutoscaleParty(Toggle):
|
||||
"""If enabled, joining party members will be scaled to roughly the level of the sphere they were obtained in."""
|
||||
display_name = "Autoscale Party Members"
|
||||
|
||||
|
||||
class ProgressiveWeapons(Toggle):
|
||||
"""If enabled, Bats, Fry Pans, and Guns will be progressive. Does not apply to items dropped by enemies or found in shops."""
|
||||
display_name = "Progressive Weapons"
|
||||
|
||||
|
||||
class ProgressiveArmor(Toggle):
|
||||
"""If enabled, Bracelets and items for the Other slot besides Ribbons will be progressive. Does not apply to items dropped by enemies or found in shops."""
|
||||
display_name = "Progressive Armor"
|
||||
|
||||
|
||||
class PresentSprites(DefaultOnToggle):
|
||||
"""If enabled, Presents, Trash cans, and chests will have their appearance modified to be indicative of the item they contain."""
|
||||
display_name = "Match Present Sprites"
|
||||
|
||||
|
||||
class NoAPPresents(Toggle):
|
||||
"""If enabled, present that contain items for other players will appear as EarthBound presents (trashcan, present, and chest) instead of Archipelago boxes.
|
||||
Does nothing if Presents Match Contents is disabled."""
|
||||
|
||||
|
||||
class ShuffleDrops(Toggle):
|
||||
"""If enabled, enemies will drop random filler items. This does not put checks on enemy drops.
|
||||
Drop rates are unchanged."""
|
||||
display_name = "Shuffle Drops"
|
||||
|
||||
|
||||
class RandomFranklinBadge(Toggle):
|
||||
"""If enabled, the Franklin Badge will reflect a randomly selected attack type. The type can be determined from the item's name, as well as the help
|
||||
text for it. The badge's function outside of battle will not change, and neither will its name outside of the game itself."""
|
||||
display_name = "Franklin Badge Protection"
|
||||
|
||||
|
||||
class CommonWeight(Range):
|
||||
"""Weight for placing a common filler item."""
|
||||
display_name = "Common Filler Weight"
|
||||
range_start = 1
|
||||
range_end = 100
|
||||
default = 80
|
||||
|
||||
|
||||
class UncommonWeight(Range):
|
||||
"""Weight for placing an uncommon filler item."""
|
||||
display_name = "Uncommon Filler Weight"
|
||||
range_start = 1
|
||||
range_end = 100
|
||||
default = 30
|
||||
|
||||
|
||||
class RareWeight(Range):
|
||||
"""Weight for placing a rare filler item."""
|
||||
display_name = "Rare Filler Weight"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 5
|
||||
|
||||
|
||||
class MoneyWeight(Range):
|
||||
"""Weight for placing money in the item pool."""
|
||||
display_name = "Money Weight"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 0
|
||||
|
||||
|
||||
class ExperienceModifier(Range):
|
||||
"""Percentage of EXP enemies give you. 100 is vanilla, after scaling, and 300 is x3."""
|
||||
display_name = "Experience Percentage"
|
||||
range_start = 100
|
||||
range_end = 300
|
||||
default = 150
|
||||
|
||||
|
||||
class StartingMoney(Range):
|
||||
"""How much money you start with."""
|
||||
display_name = "Starting Money"
|
||||
range_start = 0
|
||||
range_end = 99999
|
||||
default = 20
|
||||
|
||||
|
||||
class EasyDeaths(DefaultOnToggle):
|
||||
"""Fully revives and heals all party members after death. If off, only Ness will be healed with 0 PP."""
|
||||
display_name = "Easy Deaths"
|
||||
|
||||
|
||||
class RandomFlavors(DefaultOnToggle):
|
||||
"""Randomizes the non-plain window color options."""
|
||||
display_name = "Random Flavors"
|
||||
|
||||
|
||||
class DeathLinkMode(Choice):
|
||||
"""Controls how receiving a Deathlink functions in battle.
|
||||
Instant: The player will be instantly defeated.
|
||||
Mortal: All characters will receieve mortal damage. The player will not be able to heal until the battle is finished.
|
||||
Mortal Mercy: All characters will receieve mortal damage, but the player will be able to heal it before they die.
|
||||
Regardless of this setting, receiving a deathlink outside of battle will always instantly defeat the player."""
|
||||
display_name = "Death Link Mode"
|
||||
option_instant = 0
|
||||
option_mortal = 1
|
||||
option_mortal_mercy = 2
|
||||
default = 1
|
||||
|
||||
|
||||
class RandomBattleBG(Toggle):
|
||||
"""Generates random battle backgrounds."""
|
||||
display_name = "Randomize Battle Backgrounds"
|
||||
|
||||
|
||||
class RandomSwirlColors(Toggle):
|
||||
"""Generates random colors for pre-battle swirls."""
|
||||
display_name = "Randomize Swirl Colors"
|
||||
|
||||
|
||||
class RemoteItems(Toggle):
|
||||
"""If enabled, you will receive your own items from the server upon collecting them, rather than locally.
|
||||
This allows co-op within the same game, and protects against loss of save data.
|
||||
However, you will not be able to play offline if this is enabled."""
|
||||
display_name = "Remote Items"
|
||||
|
||||
|
||||
class PlandoLumineHallText(FreeText):
|
||||
"""Set text to be displayed at Lumine Hall. If nothing is entered, random community-submitted text will be selected instead."""
|
||||
display_name = "Lumine Hall Text Plando"
|
||||
visibility = Visibility.none
|
||||
|
||||
|
||||
class Armorizer(Choice):
|
||||
"""All equippable armor will have randomly generated attributes. This includes who can equip it, elemental resistance (and how strong that resistance is),
|
||||
defense, and the secondary stat it increases (Either Luck or Speed, depending on armor slot.) Choosing "Help!" from the Goods menu will give you exact details
|
||||
on that piece of equipment.
|
||||
Keep Type: Equipment will keep its original equipment slot. If Progressive Armor is enabled, you will get armor with progressively higher defense.
|
||||
Chaos: Equipment will have a randomly selected slot. It will try to respect the defense progressively, but the type may not match the type received."""
|
||||
display_name = "Armorizer"
|
||||
option_off = 0
|
||||
option_keep_type = 1
|
||||
option_chaos = 2
|
||||
default = 0
|
||||
|
||||
|
||||
class Weaponizer(Choice):
|
||||
"""All weapons will have randomly generated attributes. This includes offense, guts boost, and miss rate.
|
||||
Keep Type: Equipment will keep the character that was originally able to use it. If Progressive Weapons is enabled, you will get weapons with progressively higher offense.
|
||||
Chaos: Equipment will be able to be equipped by a randomly selected character. It will try to respect the offense progresively, but the type may not match the type recieved.
|
||||
The Tee Ball Bat will always be a weapon for Ness."""
|
||||
display_name = "Weaponizer"
|
||||
option_off = 0
|
||||
option_keep_type = 1
|
||||
option_chaos = 2
|
||||
default = 0
|
||||
|
||||
|
||||
class ElementChance(Range):
|
||||
"""Percent chance for any given Body/Other equipment to have elemental protection.
|
||||
Affects Armorizer only."""
|
||||
display_name = "Elemental Resistance Chance"
|
||||
range_start = 1
|
||||
range_end = 50
|
||||
default = 15
|
||||
|
||||
|
||||
class NoFreeSancs(Toggle):
|
||||
"""If enabled, the entrance to Lilliput Steps and Fire Spring will be locked and require extra key items to access.
|
||||
These items are the Tiny Key and Tenda Lavapants, respectively."""
|
||||
display_name = "No Free Sanctuaries"
|
||||
|
||||
|
||||
class RandomizeFanfares(Choice):
|
||||
"""Randomizes fanfares."""
|
||||
display_name = "Randomize Fanfares"
|
||||
option_off = 0
|
||||
option_on = 1
|
||||
option_on_no_sound_stone_fanfares = 2
|
||||
default = 0
|
||||
|
||||
|
||||
class RandomizeBattleMusic(Toggle):
|
||||
"""Randomizes in-battle songs."""
|
||||
display_name = "Randomize Battle Music"
|
||||
|
||||
|
||||
class RandomizeOverworldMusic(Choice):
|
||||
"""Randomizes music on the overworld. Some sound effects might sound weird.
|
||||
Normal: Does not randomize music.
|
||||
Match Type: Music will be randomized with similar song categories (Town, dungeon, etc.)
|
||||
Full: Overworld music will be randomized disregarding categories."""
|
||||
display_name = "Overworld Music Randomizer"
|
||||
option_normal = 0
|
||||
option_match_type = 1
|
||||
option_full = 2
|
||||
default = 0
|
||||
|
||||
|
||||
class RandomizePSIPalettes(Choice):
|
||||
"""Randomizes the colors of PSI spells.
|
||||
Normal: Doesn't randomize PSI colors.
|
||||
Shuffled: PSI spell palettes are swapped around with each other.
|
||||
Randomized: PSI spells use completely random colors."""
|
||||
display_name = "Random PSI Palettes"
|
||||
option_normal = 0
|
||||
option_shuffled = 1
|
||||
option_randomized = 2
|
||||
default = 0
|
||||
|
||||
|
||||
class ShopRandomizer(Choice):
|
||||
"""Randomizes items in shops.
|
||||
Local Filler: Shops contain only random items for yourself and are not checks.
|
||||
Shopsanity. Every shop slot in the game contains a Multiworld location. ONLY ENABLE SHOPSANITY IF YOU KNOW WHAT YOU ARE DOING."""
|
||||
display_name = "Shop Randomizer"
|
||||
option_off = 0
|
||||
option_local_filler = 1
|
||||
option_shopsanity = 2
|
||||
default = 0
|
||||
|
||||
|
||||
class ScoutShopChecks(Choice):
|
||||
"""Scouts Shop checks when you open a shop. Only affects shops in Shopsanity mode."""
|
||||
display_name = "Scout Shop Checks"
|
||||
option_off = 0
|
||||
option_progression_only = 1
|
||||
option_all = 2
|
||||
default = 1
|
||||
|
||||
|
||||
class StartingCharacter(Choice):
|
||||
"""Sets which character you start as. Each character will always start with the ability to teleport,
|
||||
and the ATM card. Ness will not be required to fight Sanctuary bosses."""
|
||||
display_name = "Starting Character"
|
||||
option_Ness = 0
|
||||
option_Paula = 1
|
||||
option_Jeff = 2
|
||||
option_Poo = 3
|
||||
default = 0
|
||||
|
||||
|
||||
class EquipamizerStatCap(DefaultOnToggle):
|
||||
"""If enabled, the highest value that Equipamizer can roll for a piece of equipment's
|
||||
main stat will be capped. 80 for armor, 125 for weapons.
|
||||
If disabled, the main stat can potentially roll up to 128."""
|
||||
display_name = "Equipamizer Stat Cap"
|
||||
|
||||
|
||||
class MoneyDropMultiplier(Range):
|
||||
"""Multiplies money dropped by enemies by the chosen value."""
|
||||
display_name = "Money Drop Multiplier"
|
||||
range_start = 1
|
||||
range_end = 100
|
||||
default = 1
|
||||
|
||||
|
||||
class EnemyShuffle(Toggle):
|
||||
"""Shuffles Non-boss enemies amongst each other."""
|
||||
display_name = "Enemy Shuffle"
|
||||
|
||||
|
||||
class SkipEpilogue(Toggle):
|
||||
"""If enabled, the choice to play the epilogue after beating Giygas will be removed, and you will
|
||||
go directly to the credits. This option is mainly for no-release seeds where checks could be
|
||||
potentially spoiled in the open-access epilogue."""
|
||||
display_name = "Skip Epilogue"
|
||||
visibility = Visibility.template
|
||||
|
||||
|
||||
class EnergyLink(Toggle):
|
||||
"""If enabled, the money in the ATM will be linked across the Archipelago Server.
|
||||
This requires a server connection to be used, but won't break offline play."""
|
||||
display_name = "Energy Link"
|
||||
|
||||
|
||||
class DungeonShuffle(Toggle):
|
||||
"""Shuffles Dungeon entrances amongst each other."""
|
||||
display_name = "Dungeon Shuffle"
|
||||
|
||||
|
||||
class PhotoCount(Range):
|
||||
"""How many Photograph traps are placed in the item pool."""
|
||||
display_name = "Photos in pool"
|
||||
range_start = 0
|
||||
range_end = 32
|
||||
default = 20
|
||||
|
||||
|
||||
class EasyCombat(Toggle):
|
||||
"""Automatically halves all scaled enemy levels."""
|
||||
display_name = "Easy Combat"
|
||||
|
||||
|
||||
class EnemizerStats(Toggle):
|
||||
"""Randomizes base stats and level of non-boss enemies."""
|
||||
display_name = "Randomize Enemy Stats"
|
||||
|
||||
|
||||
class EnemizerAttacks(Toggle):
|
||||
"""Randomizes attacks of non-boss enemies."""
|
||||
display_name = "Randomize Enemy Attacks"
|
||||
|
||||
|
||||
class EnemizerAttributes(Toggle):
|
||||
"""Randomizes most attributes of non-boss enemies."""
|
||||
display_name = "Randomize Enemy Attributes"
|
||||
|
||||
|
||||
class RandomMapColors(Choice):
|
||||
"""Randomizes map colors.
|
||||
Normal: Uses normal colors
|
||||
Nice: Uses generally good looking palettes for maps with little artifacting.
|
||||
Ugly: Allows map palettes with artifacting or colors that may not look good.
|
||||
Nonsense: Allows really bad palettes or heavy artifacting."""
|
||||
display_name = "Shuffle Map Palettes"
|
||||
option_normal = 0
|
||||
option_nice = 1
|
||||
option_ugly = 2
|
||||
option_nonsense = 3
|
||||
default = 0
|
||||
|
||||
class SafeFinalBoss(DefaultOnToggle):
|
||||
"""Prevents specific difficult bosses from being randomized onto Heavily Armed Pokey's boss slot.
|
||||
Only affects Boss Shuffle, and does not affect Phase 2 Giygas if Boss Shuffle Add Giygas is enabled."""
|
||||
display_name = "Safe Final Boss"
|
||||
|
||||
|
||||
@dataclass
|
||||
class EBOptions(PerGameCommonOptions):
|
||||
giygas_required: GiygasRequired
|
||||
sanctuaries_required: SanctuariesRequired
|
||||
skip_prayer_sequences: ShortenPrayers
|
||||
random_start_location: RandomStartLocation
|
||||
alternate_sanctuary_goal: SanctuaryAltGoal
|
||||
magicant_mode: MagicantMode
|
||||
monkey_caves_mode: MonkeyCavesMode
|
||||
local_teleports: LocalTeleports
|
||||
character_shuffle: CharacterShuffle
|
||||
starting_character: StartingCharacter
|
||||
psi_shuffle: PSIShuffle
|
||||
allow_flash_as_favorite_thing: BanFlashFavorite
|
||||
enemy_shuffle: EnemyShuffle
|
||||
boss_shuffle: BossShuffle
|
||||
decouple_diamond_dog: DecoupleDiamondDog
|
||||
boss_shuffle_add_giygas: ShuffleGiygas
|
||||
safe_final_boss: SafeFinalBoss
|
||||
randomize_enemy_attributes: EnemizerAttributes
|
||||
randomize_enemy_stats: EnemizerStats
|
||||
randomize_enemy_attacks: EnemizerAttacks
|
||||
experience_modifier: ExperienceModifier
|
||||
money_drop_multiplier: MoneyDropMultiplier
|
||||
starting_money: StartingMoney
|
||||
easy_deaths: EasyDeaths
|
||||
easy_combat: EasyCombat
|
||||
progressive_weapons: ProgressiveWeapons
|
||||
progressive_armor: ProgressiveArmor
|
||||
armorizer: Armorizer
|
||||
weaponizer: Weaponizer
|
||||
armorizer_resistance_chance: ElementChance
|
||||
equipamizer_cap_stats: EquipamizerStatCap
|
||||
auto_scale_party_members: AutoscaleParty
|
||||
remote_items: RemoteItems
|
||||
random_flavors: RandomFlavors
|
||||
random_battle_backgrounds: RandomBattleBG
|
||||
random_swirl_colors: RandomSwirlColors
|
||||
presents_match_contents: PresentSprites
|
||||
nonlocal_items_use_local_presents: NoAPPresents
|
||||
prefixed_items: PreFixItems
|
||||
total_photos: PhotoCount
|
||||
randomize_franklinbadge_protection: RandomFranklinBadge
|
||||
shuffle_enemy_drops: ShuffleDrops
|
||||
common_filler_weight: CommonWeight
|
||||
uncommon_filler_weight: UncommonWeight
|
||||
rare_filler_weight: RareWeight
|
||||
money_weight: MoneyWeight
|
||||
plando_lumine_hall_text: PlandoLumineHallText
|
||||
no_free_sanctuaries: NoFreeSancs
|
||||
randomize_overworld_music: RandomizeOverworldMusic
|
||||
randomize_battle_music: RandomizeBattleMusic
|
||||
randomize_fanfares: RandomizeFanfares
|
||||
randomize_psi_palettes: RandomizePSIPalettes
|
||||
map_palette_shuffle: RandomMapColors
|
||||
shop_randomizer: ShopRandomizer
|
||||
scout_shop_checks: ScoutShopChecks
|
||||
dungeon_shuffle: DungeonShuffle
|
||||
skip_epilogue: SkipEpilogue
|
||||
start_inventory_from_pool: StartInventoryPool
|
||||
death_link: DeathLink
|
||||
death_link_mode: DeathLinkMode
|
||||
energy_link: EnergyLink
|
||||
|
||||
|
||||
eb_option_groups = [
|
||||
OptionGroup("Goal Settings", [
|
||||
GiygasRequired,
|
||||
SanctuariesRequired,
|
||||
SanctuaryAltGoal
|
||||
]),
|
||||
|
||||
OptionGroup("Item Settings", [
|
||||
LocalTeleports,
|
||||
CharacterShuffle,
|
||||
ProgressiveWeapons,
|
||||
ProgressiveArmor,
|
||||
RandomFranklinBadge,
|
||||
CommonWeight,
|
||||
UncommonWeight,
|
||||
RareWeight,
|
||||
MoneyWeight,
|
||||
PreFixItems,
|
||||
PhotoCount
|
||||
]),
|
||||
|
||||
OptionGroup("Equipamizer", [
|
||||
Armorizer,
|
||||
Weaponizer,
|
||||
ElementChance,
|
||||
EquipamizerStatCap
|
||||
]),
|
||||
|
||||
OptionGroup("World Modes", [
|
||||
RandomStartLocation,
|
||||
MagicantMode,
|
||||
MonkeyCavesMode,
|
||||
NoFreeSancs,
|
||||
StartingCharacter
|
||||
]),
|
||||
|
||||
OptionGroup("PSI Randomization", [
|
||||
PSIShuffle,
|
||||
BanFlashFavorite
|
||||
]),
|
||||
|
||||
OptionGroup("Enemy Randomization", [
|
||||
EnemyShuffle,
|
||||
BossShuffle,
|
||||
SafeFinalBoss,
|
||||
DecoupleDiamondDog,
|
||||
ShuffleGiygas,
|
||||
ExperienceModifier,
|
||||
ShuffleDrops,
|
||||
MoneyDropMultiplier
|
||||
]),
|
||||
|
||||
OptionGroup("Enemizer", [
|
||||
EnemizerAttributes,
|
||||
EnemizerAttacks,
|
||||
EnemizerStats
|
||||
]),
|
||||
|
||||
OptionGroup("Shop Randomization", [
|
||||
ShopRandomizer,
|
||||
ScoutShopChecks
|
||||
]),
|
||||
|
||||
OptionGroup("Entrance Randomization", [
|
||||
DungeonShuffle
|
||||
]),
|
||||
|
||||
OptionGroup("Convenience Settings", [
|
||||
ShortenPrayers,
|
||||
EasyDeaths,
|
||||
StartingMoney,
|
||||
RemoteItems,
|
||||
AutoscaleParty,
|
||||
SkipEpilogue,
|
||||
EasyCombat
|
||||
]),
|
||||
|
||||
OptionGroup("Aesthetic Settings", [
|
||||
RandomFlavors,
|
||||
RandomSwirlColors,
|
||||
RandomBattleBG,
|
||||
RandomMapColors,
|
||||
PresentSprites,
|
||||
NoAPPresents,
|
||||
RandomizePSIPalettes,
|
||||
PlandoLumineHallText
|
||||
]),
|
||||
|
||||
OptionGroup("Music Randomizer", [
|
||||
RandomizeOverworldMusic,
|
||||
RandomizeBattleMusic,
|
||||
RandomizeFanfares
|
||||
]),
|
||||
|
||||
OptionGroup("Multiplayer Features", [
|
||||
DeathLink,
|
||||
DeathLinkMode,
|
||||
EnergyLink
|
||||
])
|
||||
]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user