mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-10 09:33:46 -07:00
Compare commits
78 Commits
core_dyn_l
...
NewSoupVi-
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
a9e79854a8 | ||
|
|
144d612c52 | ||
|
|
3acbe9ece1 | ||
|
|
7d0b701a2d | ||
|
|
f91537fb48 | ||
|
|
3c5ec49dbe | ||
|
|
9a37a136a1 | ||
|
|
54a0a5ac00 | ||
|
|
704f14ffcd | ||
|
|
925fb967d3 | ||
|
|
5dd19fccd0 | ||
|
|
781100a571 | ||
|
|
3fb0b57d19 | ||
|
|
f79657b41a | ||
|
|
4a5ba756b6 | ||
|
|
0b3d34ab24 | ||
|
|
2e81774a1f | ||
|
|
aa22b62b41 | ||
|
|
51c4fe8f67 | ||
|
|
26f9720e69 | ||
|
|
1f712d9a87 | ||
|
|
5b4d7c7526 | ||
|
|
a948697f3a | ||
|
|
e3b5451672 | ||
|
|
6c69f590cf | ||
|
|
c9625e1b35 | ||
|
|
ced93022b6 | ||
|
|
f4b926ebbe | ||
|
|
203d89d1d3 | ||
|
|
4d42814f5d | ||
|
|
d80069385d | ||
|
|
85a0d59f73 | ||
|
|
58f2205304 | ||
|
|
769fbc55a9 | ||
|
|
f43fa612d5 | ||
|
|
5b0de6b6c7 | ||
|
|
ac8a206d46 | ||
|
|
6896d631db | ||
|
|
6f2e1c2a7e | ||
|
|
ffe0221deb | ||
|
|
18e8d50768 | ||
|
|
81b9a53a37 | ||
|
|
b6ab91fe4b | ||
|
|
f26cda07db | ||
|
|
409c915375 | ||
|
|
ecc3094c70 | ||
|
|
17b3ee6eaf | ||
|
|
284e7797c5 | ||
|
|
62ce42440b | ||
|
|
7b755408fa | ||
|
|
ed721dd0c1 | ||
|
|
1a5d22ca78 | ||
|
|
21dbfd2472 | ||
|
|
472d2d5406 | ||
|
|
3af2b1dc66 | ||
|
|
6cfc3a4667 | ||
|
|
992657750c | ||
|
|
a67688749f | ||
|
|
f735416bda | ||
|
|
e5374eb8b8 | ||
|
|
b83b48629d | ||
|
|
ca6792a8a7 | ||
|
|
7cbd50a2e6 | ||
|
|
d6da3bc899 | ||
|
|
9eaca95277 | ||
|
|
c1b27f79ac | ||
|
|
0705f6e6c0 | ||
|
|
a537d8eb65 | ||
|
|
845a604955 | ||
|
|
7adb673a80 | ||
|
|
72e88bb493 | ||
|
|
089b3f17a7 | ||
|
|
ad30e3264a | ||
|
|
e262c8be9c | ||
|
|
492e3a355e | ||
|
|
1487d323cd | ||
|
|
dd88b2c658 | ||
|
|
46dfc4d4fc |
6
.github/workflows/build.yml
vendored
6
.github/workflows/build.yml
vendored
@@ -31,7 +31,8 @@ jobs:
|
||||
- name: Install python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
python-version: '~3.12.7'
|
||||
check-latest: true
|
||||
- name: Download run-time dependencies
|
||||
run: |
|
||||
Invoke-WebRequest -Uri https://github.com/Ijwu/Enemizer/releases/download/${Env:ENEMIZER_VERSION}/win-x64.zip -OutFile enemizer.zip
|
||||
@@ -111,7 +112,8 @@ jobs:
|
||||
- name: Get a recent python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
python-version: '~3.12.7'
|
||||
check-latest: true
|
||||
- name: Install build-time dependencies
|
||||
run: |
|
||||
echo "PYTHON=python3.12" >> $GITHUB_ENV
|
||||
|
||||
3
.github/workflows/release.yml
vendored
3
.github/workflows/release.yml
vendored
@@ -44,7 +44,8 @@ jobs:
|
||||
- name: Get a recent python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.12'
|
||||
python-version: '~3.12.7'
|
||||
check-latest: true
|
||||
- name: Install build-time dependencies
|
||||
run: |
|
||||
echo "PYTHON=python3.12" >> $GITHUB_ENV
|
||||
|
||||
6
.github/workflows/scan-build.yml
vendored
6
.github/workflows/scan-build.yml
vendored
@@ -40,10 +40,10 @@ jobs:
|
||||
run: |
|
||||
wget https://apt.llvm.org/llvm.sh
|
||||
chmod +x ./llvm.sh
|
||||
sudo ./llvm.sh 17
|
||||
sudo ./llvm.sh 19
|
||||
- name: Install scan-build command
|
||||
run: |
|
||||
sudo apt install clang-tools-17
|
||||
sudo apt install clang-tools-19
|
||||
- name: Get a recent python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
@@ -56,7 +56,7 @@ jobs:
|
||||
- name: scan-build
|
||||
run: |
|
||||
source venv/bin/activate
|
||||
scan-build-17 --status-bugs -o scan-build-reports -disable-checker deadcode.DeadStores python setup.py build -y
|
||||
scan-build-19 --status-bugs -o scan-build-reports -disable-checker deadcode.DeadStores python setup.py build -y
|
||||
- name: Store report
|
||||
if: failure()
|
||||
uses: actions/upload-artifact@v4
|
||||
|
||||
@@ -604,6 +604,49 @@ class MultiWorld():
|
||||
state.collect(location.item, True, location)
|
||||
locations -= sphere
|
||||
|
||||
def get_sendable_spheres(self) -> Iterator[Set[Location]]:
|
||||
"""
|
||||
yields a set of multiserver sendable locations (location.item.code: int) for each logical sphere
|
||||
|
||||
If there are unreachable locations, the last sphere of reachable locations is followed by an empty set,
|
||||
and then a set of all of the unreachable locations.
|
||||
"""
|
||||
state = CollectionState(self)
|
||||
locations: Set[Location] = set()
|
||||
events: Set[Location] = set()
|
||||
for location in self.get_filled_locations():
|
||||
if type(location.item.code) is int:
|
||||
locations.add(location)
|
||||
else:
|
||||
events.add(location)
|
||||
|
||||
while locations:
|
||||
sphere: Set[Location] = set()
|
||||
|
||||
# cull events out
|
||||
done_events: Set[Union[Location, None]] = {None}
|
||||
while done_events:
|
||||
done_events = set()
|
||||
for event in events:
|
||||
if event.can_reach(state):
|
||||
state.collect(event.item, True, event)
|
||||
done_events.add(event)
|
||||
events -= done_events
|
||||
|
||||
for location in locations:
|
||||
if location.can_reach(state):
|
||||
sphere.add(location)
|
||||
|
||||
yield sphere
|
||||
if not sphere:
|
||||
if locations:
|
||||
yield locations # unreachable locations
|
||||
break
|
||||
|
||||
for location in sphere:
|
||||
state.collect(location.item, True, location)
|
||||
locations -= sphere
|
||||
|
||||
def fulfills_accessibility(self, state: Optional[CollectionState] = None):
|
||||
"""Check if accessibility rules are fulfilled with current or supplied state."""
|
||||
if not state:
|
||||
|
||||
20
Fill.py
20
Fill.py
@@ -36,7 +36,8 @@ def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item]
|
||||
def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locations: typing.List[Location],
|
||||
item_pool: typing.List[Item], single_player_placement: bool = False, lock: bool = False,
|
||||
swap: bool = True, on_place: typing.Optional[typing.Callable[[Location], None]] = None,
|
||||
allow_partial: bool = False, allow_excluded: bool = False, name: str = "Unknown") -> None:
|
||||
allow_partial: bool = False, allow_excluded: bool = False, one_item_per_player: bool = True,
|
||||
name: str = "Unknown") -> None:
|
||||
"""
|
||||
:param multiworld: Multiworld to be filled.
|
||||
:param base_state: State assumed before fill.
|
||||
@@ -63,14 +64,22 @@ def fill_restrictive(multiworld: MultiWorld, base_state: CollectionState, locati
|
||||
placed = 0
|
||||
|
||||
while any(reachable_items.values()) and locations:
|
||||
# grab one item per player
|
||||
items_to_place = [items.pop()
|
||||
for items in reachable_items.values() if items]
|
||||
if one_item_per_player:
|
||||
# grab one item per player
|
||||
items_to_place = [items.pop()
|
||||
for items in reachable_items.values() if items]
|
||||
else:
|
||||
next_player = multiworld.random.choice([player for player, items in reachable_items.items() if items])
|
||||
items_to_place = []
|
||||
if item_pool:
|
||||
items_to_place.append(reachable_items[next_player].pop())
|
||||
|
||||
for item in items_to_place:
|
||||
for p, pool_item in enumerate(item_pool):
|
||||
if pool_item is item:
|
||||
item_pool.pop(p)
|
||||
break
|
||||
|
||||
maximum_exploration_state = sweep_from_pool(
|
||||
base_state, item_pool + unplaced_items, multiworld.get_filled_locations(item.player)
|
||||
if single_player_placement else None)
|
||||
@@ -480,7 +489,8 @@ def distribute_items_restrictive(multiworld: MultiWorld,
|
||||
if prioritylocations:
|
||||
# "priority fill"
|
||||
fill_restrictive(multiworld, multiworld.state, prioritylocations, progitempool,
|
||||
single_player_placement=single_player, swap=False, on_place=mark_for_locking, name="Priority")
|
||||
single_player_placement=single_player, swap=False, on_place=mark_for_locking,
|
||||
name="Priority", one_item_per_player=False)
|
||||
accessibility_corrections(multiworld, multiworld.state, prioritylocations, progitempool)
|
||||
defaultlocations = prioritylocations + defaultlocations
|
||||
|
||||
|
||||
@@ -114,7 +114,14 @@ def main(args=None) -> Tuple[argparse.Namespace, int]:
|
||||
os.path.join(args.player_files_path, fname) not in {args.meta_file_path, args.weights_file_path}:
|
||||
path = os.path.join(args.player_files_path, fname)
|
||||
try:
|
||||
weights_cache[fname] = read_weights_yamls(path)
|
||||
weights_for_file = []
|
||||
for doc_idx, yaml in enumerate(read_weights_yamls(path)):
|
||||
if yaml is None:
|
||||
logging.warning(f"Ignoring empty yaml document #{doc_idx + 1} in {fname}")
|
||||
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
|
||||
|
||||
|
||||
65
Launcher.py
65
Launcher.py
@@ -126,12 +126,13 @@ def handle_uri(path: str, launch_args: Tuple[str, ...]) -> None:
|
||||
elif component.display_name == "Text Client":
|
||||
text_client_component = component
|
||||
|
||||
from kvui import App, Button, BoxLayout, Label, Clock, Window
|
||||
if client_component is None:
|
||||
run_component(text_client_component, *launch_args)
|
||||
return
|
||||
|
||||
from kvui import App, Button, BoxLayout, Label, Window
|
||||
|
||||
class Popup(App):
|
||||
timer_label: Label
|
||||
remaining_time: Optional[int]
|
||||
|
||||
def __init__(self):
|
||||
self.title = "Connect to Multiworld"
|
||||
self.icon = r"data/icon.png"
|
||||
@@ -139,48 +140,25 @@ def handle_uri(path: str, launch_args: Tuple[str, ...]) -> None:
|
||||
|
||||
def build(self):
|
||||
layout = BoxLayout(orientation="vertical")
|
||||
layout.add_widget(Label(text="Select client to open and connect with."))
|
||||
button_row = BoxLayout(orientation="horizontal", size_hint=(1, 0.4))
|
||||
|
||||
if client_component is None:
|
||||
self.remaining_time = 7
|
||||
label_text = (f"A game client able to parse URIs was not detected for {game}.\n"
|
||||
f"Launching Text Client in 7 seconds...")
|
||||
self.timer_label = Label(text=label_text)
|
||||
layout.add_widget(self.timer_label)
|
||||
Clock.schedule_interval(self.update_label, 1)
|
||||
else:
|
||||
layout.add_widget(Label(text="Select client to open and connect with."))
|
||||
button_row = BoxLayout(orientation="horizontal", size_hint=(1, 0.4))
|
||||
text_client_button = Button(
|
||||
text=text_client_component.display_name,
|
||||
on_release=lambda *args: run_component(text_client_component, *launch_args)
|
||||
)
|
||||
button_row.add_widget(text_client_button)
|
||||
|
||||
text_client_button = Button(
|
||||
text=text_client_component.display_name,
|
||||
on_release=lambda *args: run_component(text_client_component, *launch_args)
|
||||
)
|
||||
button_row.add_widget(text_client_button)
|
||||
game_client_button = Button(
|
||||
text=client_component.display_name,
|
||||
on_release=lambda *args: run_component(client_component, *launch_args)
|
||||
)
|
||||
button_row.add_widget(game_client_button)
|
||||
|
||||
game_client_button = Button(
|
||||
text=client_component.display_name,
|
||||
on_release=lambda *args: run_component(client_component, *launch_args)
|
||||
)
|
||||
button_row.add_widget(game_client_button)
|
||||
|
||||
layout.add_widget(button_row)
|
||||
layout.add_widget(button_row)
|
||||
|
||||
return layout
|
||||
|
||||
def update_label(self, dt):
|
||||
if self.remaining_time > 1:
|
||||
# countdown the timer and string replace the number
|
||||
self.remaining_time -= 1
|
||||
self.timer_label.text = self.timer_label.text.replace(
|
||||
str(self.remaining_time + 1), str(self.remaining_time)
|
||||
)
|
||||
else:
|
||||
# our timer is finished so launch text client and close down
|
||||
run_component(text_client_component, *launch_args)
|
||||
Clock.unschedule(self.update_label)
|
||||
App.get_running_app().stop()
|
||||
Window.close()
|
||||
|
||||
def _stop(self, *largs):
|
||||
# see run_gui Launcher _stop comment for details
|
||||
self.root_window.close()
|
||||
@@ -246,9 +224,8 @@ refresh_components: Optional[Callable[[], None]] = None
|
||||
|
||||
|
||||
def run_gui():
|
||||
from kvui import App, ContainerLayout, GridLayout, Button, Label, ScrollBox, Widget
|
||||
from kvui import App, ContainerLayout, GridLayout, Button, Label, ScrollBox, Widget, ApAsyncImage
|
||||
from kivy.core.window import Window
|
||||
from kivy.uix.image import AsyncImage
|
||||
from kivy.uix.relativelayout import RelativeLayout
|
||||
|
||||
class Launcher(App):
|
||||
@@ -281,8 +258,8 @@ def run_gui():
|
||||
button.component = component
|
||||
button.bind(on_release=self.component_action)
|
||||
if component.icon != "icon":
|
||||
image = AsyncImage(source=icon_paths[component.icon],
|
||||
size=(38, 38), size_hint=(None, 1), pos=(5, 0))
|
||||
image = ApAsyncImage(source=icon_paths[component.icon],
|
||||
size=(38, 38), size_hint=(None, 1), pos=(5, 0))
|
||||
box_layout = RelativeLayout(size_hint_y=None, height=40)
|
||||
box_layout.add_widget(button)
|
||||
box_layout.add_widget(image)
|
||||
|
||||
21
Main.py
21
Main.py
@@ -242,6 +242,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
|
||||
def write_multidata():
|
||||
import NetUtils
|
||||
from NetUtils import HintStatus
|
||||
slot_data = {}
|
||||
client_versions = {}
|
||||
games = {}
|
||||
@@ -266,10 +267,10 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
for slot in multiworld.player_ids:
|
||||
slot_data[slot] = multiworld.worlds[slot].fill_slot_data()
|
||||
|
||||
def precollect_hint(location):
|
||||
def precollect_hint(location: Location, auto_status: HintStatus):
|
||||
entrance = er_hint_data.get(location.player, {}).get(location.address, "")
|
||||
hint = NetUtils.Hint(location.item.player, location.player, location.address,
|
||||
location.item.code, False, entrance, location.item.flags, False)
|
||||
location.item.code, False, entrance, location.item.flags, auto_status)
|
||||
precollected_hints[location.player].add(hint)
|
||||
if location.item.player not in multiworld.groups:
|
||||
precollected_hints[location.item.player].add(hint)
|
||||
@@ -282,19 +283,22 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
if type(location.address) == int:
|
||||
assert location.item.code is not None, "item code None should be event, " \
|
||||
"location.address should then also be None. Location: " \
|
||||
f" {location}"
|
||||
f" {location}, Item: {location.item}"
|
||||
assert location.address not in locations_data[location.player], (
|
||||
f"Locations with duplicate address. {location} and "
|
||||
f"{locations_data[location.player][location.address]}")
|
||||
locations_data[location.player][location.address] = \
|
||||
location.item.code, location.item.player, location.item.flags
|
||||
auto_status = HintStatus.HINT_AVOID if location.item.trap else HintStatus.HINT_PRIORITY
|
||||
if location.name in multiworld.worlds[location.player].options.start_location_hints:
|
||||
precollect_hint(location)
|
||||
if not location.item.trap: # Unspecified status for location hints, except traps
|
||||
auto_status = HintStatus.HINT_UNSPECIFIED
|
||||
precollect_hint(location, auto_status)
|
||||
elif location.item.name in multiworld.worlds[location.item.player].options.start_hints:
|
||||
precollect_hint(location)
|
||||
precollect_hint(location, auto_status)
|
||||
elif any([location.item.name in multiworld.worlds[player].options.start_hints
|
||||
for player in multiworld.groups.get(location.item.player, {}).get("players", [])]):
|
||||
precollect_hint(location)
|
||||
precollect_hint(location, auto_status)
|
||||
|
||||
# embedded data package
|
||||
data_package = {
|
||||
@@ -306,11 +310,10 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
|
||||
# get spheres -> filter address==None -> skip empty
|
||||
spheres: List[Dict[int, Set[int]]] = []
|
||||
for sphere in multiworld.get_spheres():
|
||||
for sphere in multiworld.get_sendable_spheres():
|
||||
current_sphere: Dict[int, Set[int]] = collections.defaultdict(set)
|
||||
for sphere_location in sphere:
|
||||
if type(sphere_location.address) is int:
|
||||
current_sphere[sphere_location.player].add(sphere_location.address)
|
||||
current_sphere[sphere_location.player].add(sphere_location.address)
|
||||
|
||||
if current_sphere:
|
||||
spheres.append(dict(current_sphere))
|
||||
|
||||
@@ -5,8 +5,15 @@ import multiprocessing
|
||||
import warnings
|
||||
|
||||
|
||||
if sys.version_info < (3, 10, 11):
|
||||
raise RuntimeError(f"Incompatible Python Version found: {sys.version_info}. 3.10.11+ is supported.")
|
||||
if sys.platform in ("win32", "darwin") and sys.version_info < (3, 10, 11):
|
||||
# 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.10.15+ is supported.")
|
||||
elif sys.platform in ("win32", "darwin") and sys.version_info < (3, 10, 15):
|
||||
# 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, 10, 1):
|
||||
# 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.10.1+ is supported.")
|
||||
|
||||
# don't run update if environment is frozen/compiled or if not the parent process (skip in subprocess)
|
||||
_skip_update = bool(getattr(sys, "frozen", False) or multiprocessing.parent_process())
|
||||
|
||||
@@ -975,9 +975,13 @@ def get_status_string(ctx: Context, team: int, tag: str):
|
||||
tagged = len([client for client in ctx.clients[team][slot] if tag in client.tags])
|
||||
completion_text = f"({len(ctx.location_checks[team, slot])}/{len(ctx.locations[slot])})"
|
||||
tag_text = f" {tagged} of which are tagged {tag}" if connected and tag else ""
|
||||
goal_text = " and has finished." if ctx.client_game_state[team, slot] == ClientStatus.CLIENT_GOAL else "."
|
||||
status_text = (
|
||||
" and has finished." if ctx.client_game_state[team, slot] == ClientStatus.CLIENT_GOAL else
|
||||
" and is ready." if ctx.client_game_state[team, slot] == ClientStatus.CLIENT_READY else
|
||||
"."
|
||||
)
|
||||
text += f"\n{ctx.get_aliased_name(team, slot)} has {connected} connection{'' if connected == 1 else 's'}" \
|
||||
f"{tag_text}{goal_text} {completion_text}"
|
||||
f"{tag_text}{status_text} {completion_text}"
|
||||
return text
|
||||
|
||||
|
||||
@@ -1910,7 +1914,7 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||
hint = ctx.get_hint(client.team, player, location)
|
||||
if not hint:
|
||||
return # Ignored safely
|
||||
if hint.receiving_player != client.slot:
|
||||
if client.slot not in ctx.slot_set(hint.receiving_player):
|
||||
await ctx.send_msgs(client,
|
||||
[{'cmd': 'InvalidPacket', "type": "arguments", "text": 'UpdateHint: No Permission',
|
||||
"original_cmd": cmd}])
|
||||
@@ -1925,6 +1929,11 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||
[{'cmd': 'InvalidPacket', "type": "arguments",
|
||||
"text": 'UpdateHint: Invalid Status', "original_cmd": cmd}])
|
||||
return
|
||||
if status == HintStatus.HINT_FOUND:
|
||||
await ctx.send_msgs(client,
|
||||
[{'cmd': 'InvalidPacket', "type": "arguments",
|
||||
"text": 'UpdateHint: Cannot manually update status to "HINT_FOUND"', "original_cmd": cmd}])
|
||||
return
|
||||
new_hint = new_hint.re_prioritize(ctx, status)
|
||||
if hint == new_hint:
|
||||
return
|
||||
@@ -2374,6 +2383,8 @@ def parse_args() -> argparse.Namespace:
|
||||
parser.add_argument('--cert_key', help="Path to SSL Certificate Key file")
|
||||
parser.add_argument('--loglevel', default=defaults["loglevel"],
|
||||
choices=['debug', 'info', 'warning', 'error', 'critical'])
|
||||
parser.add_argument('--logtime', help="Add timestamps to STDOUT",
|
||||
default=defaults["logtime"], action='store_true')
|
||||
parser.add_argument('--location_check_points', default=defaults["location_check_points"], type=int)
|
||||
parser.add_argument('--hint_cost', default=defaults["hint_cost"], type=int)
|
||||
parser.add_argument('--disable_item_cheat', default=defaults["disable_item_cheat"], action='store_true')
|
||||
@@ -2454,7 +2465,9 @@ def load_server_cert(path: str, cert_key: typing.Optional[str]) -> "ssl.SSLConte
|
||||
|
||||
|
||||
async def main(args: argparse.Namespace):
|
||||
Utils.init_logging("Server", loglevel=args.loglevel.lower())
|
||||
Utils.init_logging(name="Server",
|
||||
loglevel=args.loglevel.lower(),
|
||||
add_timestamp=args.logtime)
|
||||
|
||||
ctx = Context(args.host, args.port, args.server_password, args.password, args.location_check_points,
|
||||
args.hint_cost, not args.disable_item_cheat, args.release_mode, args.collect_mode,
|
||||
|
||||
@@ -232,7 +232,7 @@ class JSONtoTextParser(metaclass=HandlerMeta):
|
||||
|
||||
def _handle_player_id(self, node: JSONMessagePart):
|
||||
player = int(node["text"])
|
||||
node["color"] = 'magenta' if player == self.ctx.slot else 'yellow'
|
||||
node["color"] = 'magenta' if self.ctx.slot_concerns_self(player) else 'yellow'
|
||||
node["text"] = self.ctx.player_names[player]
|
||||
return self._handle_color(node)
|
||||
|
||||
@@ -410,6 +410,8 @@ class _LocationStore(dict, typing.MutableMapping[int, typing.Dict[int, typing.Tu
|
||||
checked = state[team, slot]
|
||||
if not checked:
|
||||
# This optimizes the case where everyone connects to a fresh game at the same time.
|
||||
if slot not in self:
|
||||
raise KeyError(slot)
|
||||
return []
|
||||
return [location_id for
|
||||
location_id in self[slot] if
|
||||
|
||||
48
Options.py
48
Options.py
@@ -754,7 +754,7 @@ class NamedRange(Range):
|
||||
elif value > self.range_end and value not in self.special_range_names.values():
|
||||
raise Exception(f"{value} is higher than maximum {self.range_end} for option {self.__class__.__name__} " +
|
||||
f"and is also not one of the supported named special values: {self.special_range_names}")
|
||||
|
||||
|
||||
# See docstring
|
||||
for key in self.special_range_names:
|
||||
if key != key.lower():
|
||||
@@ -863,6 +863,8 @@ class ItemDict(OptionDict):
|
||||
verify_item_name = True
|
||||
|
||||
def __init__(self, value: typing.Dict[str, int]):
|
||||
if any(item_count is None for item_count in value.values()):
|
||||
raise Exception("Items must have counts associated with them. Please provide positive integer values in the format \"item\": count .")
|
||||
if any(item_count < 1 for item_count in value.values()):
|
||||
raise Exception("Cannot have non-positive item counts.")
|
||||
super(ItemDict, self).__init__(value)
|
||||
@@ -1178,7 +1180,7 @@ class PlandoConnections(Option[typing.List[PlandoConnection]], metaclass=Connect
|
||||
class Accessibility(Choice):
|
||||
"""
|
||||
Set rules for reachability of your items/locations.
|
||||
|
||||
|
||||
**Full:** ensure everything can be reached and acquired.
|
||||
|
||||
**Minimal:** ensure what is needed to reach your goal can be acquired.
|
||||
@@ -1196,7 +1198,7 @@ class Accessibility(Choice):
|
||||
class ItemsAccessibility(Accessibility):
|
||||
"""
|
||||
Set rules for reachability of your items/locations.
|
||||
|
||||
|
||||
**Full:** ensure everything can be reached and acquired.
|
||||
|
||||
**Minimal:** ensure what is needed to reach your goal can be acquired.
|
||||
@@ -1247,12 +1249,16 @@ class CommonOptions(metaclass=OptionsMetaProperty):
|
||||
progression_balancing: ProgressionBalancing
|
||||
accessibility: Accessibility
|
||||
|
||||
def as_dict(self, *option_names: str, casing: str = "snake") -> typing.Dict[str, typing.Any]:
|
||||
def as_dict(self,
|
||||
*option_names: str,
|
||||
casing: typing.Literal["snake", "camel", "pascal", "kebab"] = "snake",
|
||||
toggles_as_bools: bool = False) -> typing.Dict[str, typing.Any]:
|
||||
"""
|
||||
Returns a dictionary of [str, Option.value]
|
||||
|
||||
:param option_names: names of the options to return
|
||||
:param casing: case of the keys to return. Supports `snake`, `camel`, `pascal`, `kebab`
|
||||
:param toggles_as_bools: whether toggle options should be output as bools instead of strings
|
||||
"""
|
||||
assert option_names, "options.as_dict() was used without any option names."
|
||||
option_results = {}
|
||||
@@ -1274,6 +1280,8 @@ class CommonOptions(metaclass=OptionsMetaProperty):
|
||||
value = getattr(self, option_name).value
|
||||
if isinstance(value, set):
|
||||
value = sorted(value)
|
||||
elif toggles_as_bools and issubclass(type(self).type_hints[option_name], Toggle):
|
||||
value = bool(value)
|
||||
option_results[display_name] = value
|
||||
else:
|
||||
raise ValueError(f"{option_name} not found in {tuple(type(self).type_hints)}")
|
||||
@@ -1463,22 +1471,26 @@ it.
|
||||
def get_option_groups(world: typing.Type[World], visibility_level: Visibility = Visibility.template) -> typing.Dict[
|
||||
str, typing.Dict[str, typing.Type[Option[typing.Any]]]]:
|
||||
"""Generates and returns a dictionary for the option groups of a specified world."""
|
||||
option_groups = {option: option_group.name
|
||||
for option_group in world.web.option_groups
|
||||
for option in option_group.options}
|
||||
option_to_name = {option: option_name for option_name, option in world.options_dataclass.type_hints.items()}
|
||||
|
||||
ordered_groups = {group.name: group.options for group in world.web.option_groups}
|
||||
|
||||
# add a default option group for uncategorized options to get thrown into
|
||||
ordered_groups = ["Game Options"]
|
||||
[ordered_groups.append(group) for group in option_groups.values() if group not in ordered_groups]
|
||||
grouped_options = {group: {} for group in ordered_groups}
|
||||
for option_name, option in world.options_dataclass.type_hints.items():
|
||||
if visibility_level & option.visibility:
|
||||
grouped_options[option_groups.get(option, "Game Options")][option_name] = option
|
||||
if "Game Options" not in ordered_groups:
|
||||
grouped_options = set(option for group in ordered_groups.values() for option in group)
|
||||
ungrouped_options = [option for option in option_to_name if option not in grouped_options]
|
||||
# only add the game options group if we have ungrouped options
|
||||
if ungrouped_options:
|
||||
ordered_groups = {**{"Game Options": ungrouped_options}, **ordered_groups}
|
||||
|
||||
# if the world doesn't have any ungrouped options, this group will be empty so just remove it
|
||||
if not grouped_options["Game Options"]:
|
||||
del grouped_options["Game Options"]
|
||||
|
||||
return grouped_options
|
||||
return {
|
||||
group: {
|
||||
option_to_name[option]: option
|
||||
for option in group_options
|
||||
if (visibility_level in option.visibility and option in option_to_name)
|
||||
}
|
||||
for group, group_options in ordered_groups.items()
|
||||
}
|
||||
|
||||
|
||||
def generate_yaml_templates(target_folder: typing.Union[str, "pathlib.Path"], generate_hidden: bool = True) -> None:
|
||||
|
||||
@@ -77,6 +77,8 @@ Currently, the following games are supported:
|
||||
* Mega Man 2
|
||||
* Yacht Dice
|
||||
* Faxanadu
|
||||
* Saving Princess
|
||||
* Castlevania: Circle of the Moon
|
||||
|
||||
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
|
||||
|
||||
15
Utils.py
15
Utils.py
@@ -485,9 +485,9 @@ def get_text_after(text: str, start: str) -> str:
|
||||
loglevel_mapping = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}
|
||||
|
||||
|
||||
def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, write_mode: str = "w",
|
||||
log_format: str = "[%(name)s at %(asctime)s]: %(message)s",
|
||||
exception_logger: typing.Optional[str] = None):
|
||||
def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO,
|
||||
write_mode: str = "w", log_format: str = "[%(name)s at %(asctime)s]: %(message)s",
|
||||
add_timestamp: bool = False, exception_logger: typing.Optional[str] = None):
|
||||
import datetime
|
||||
loglevel: int = loglevel_mapping.get(loglevel, loglevel)
|
||||
log_folder = user_path("logs")
|
||||
@@ -514,14 +514,15 @@ def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, wri
|
||||
def filter(self, record: logging.LogRecord) -> bool:
|
||||
return self.condition(record)
|
||||
|
||||
file_handler.addFilter(Filter("NoStream", lambda record: not getattr(record, "NoFile", False)))
|
||||
file_handler.addFilter(Filter("NoCarriageReturn", lambda record: '\r' not in record.msg))
|
||||
file_handler.addFilter(Filter("NoStream", lambda record: not getattr(record, "NoFile", False)))
|
||||
file_handler.addFilter(Filter("NoCarriageReturn", lambda record: '\r' not in record.getMessage()))
|
||||
root_logger.addHandler(file_handler)
|
||||
if sys.stdout:
|
||||
formatter = logging.Formatter(fmt='[%(asctime)s] %(message)s', datefmt='%Y-%m-%d %H:%M:%S')
|
||||
stream_handler = logging.StreamHandler(sys.stdout)
|
||||
stream_handler.addFilter(Filter("NoFile", lambda record: not getattr(record, "NoStream", False)))
|
||||
stream_handler.setFormatter(formatter)
|
||||
if add_timestamp:
|
||||
stream_handler.setFormatter(formatter)
|
||||
root_logger.addHandler(stream_handler)
|
||||
|
||||
# Relay unhandled exceptions to logger.
|
||||
@@ -556,7 +557,7 @@ def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, wri
|
||||
import platform
|
||||
logging.info(
|
||||
f"Archipelago ({__version__}) logging initialized"
|
||||
f" on {platform.platform()}"
|
||||
f" on {platform.platform()} process {os.getpid()}"
|
||||
f" running Python {sys.version_info.major}.{sys.version_info.minor}.{sys.version_info.micro}"
|
||||
f"{' (frozen)' if is_frozen() else ''}"
|
||||
)
|
||||
|
||||
@@ -34,7 +34,7 @@ def get_app() -> "Flask":
|
||||
app.config.from_file(configpath, yaml.safe_load)
|
||||
logging.info(f"Updated config from {configpath}")
|
||||
# inside get_app() so it's usable in systems like gunicorn, which do not run WebHost.py, but import it.
|
||||
parser = argparse.ArgumentParser()
|
||||
parser = argparse.ArgumentParser(allow_abbrev=False)
|
||||
parser.add_argument('--config_override', default=None,
|
||||
help="Path to yaml config file that overrules config.yaml.")
|
||||
args = parser.parse_known_args()[0]
|
||||
|
||||
@@ -39,6 +39,8 @@ app.config["SECRET_KEY"] = bytes(socket.gethostname(), encoding="utf-8")
|
||||
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
|
||||
@@ -85,6 +87,6 @@ def register():
|
||||
|
||||
from WebHostLib.customserver import run_server_process
|
||||
# to trigger app routing picking up on it
|
||||
from . import tracker, upload, landing, check, generate, downloads, api, stats, misc, robots, options
|
||||
from . import tracker, upload, landing, check, generate, downloads, api, stats, misc, robots, options, session
|
||||
|
||||
app.register_blueprint(api.api_endpoints)
|
||||
|
||||
@@ -6,6 +6,7 @@ import multiprocessing
|
||||
import typing
|
||||
from datetime import timedelta, datetime
|
||||
from threading import Event, Thread
|
||||
from typing import Any
|
||||
from uuid import UUID
|
||||
|
||||
from pony.orm import db_session, select, commit
|
||||
@@ -53,7 +54,21 @@ def launch_generator(pool: multiprocessing.pool.Pool, generation: Generation):
|
||||
generation.state = STATE_STARTED
|
||||
|
||||
|
||||
def init_db(pony_config: dict):
|
||||
def init_generator(config: dict[str, Any]) -> None:
|
||||
try:
|
||||
import resource
|
||||
except ModuleNotFoundError:
|
||||
pass # unix only module
|
||||
else:
|
||||
# set soft limit for memory to from config (default 4GiB)
|
||||
soft_limit = config["GENERATOR_MEMORY_LIMIT"]
|
||||
old_limit, hard_limit = resource.getrlimit(resource.RLIMIT_AS)
|
||||
if soft_limit != old_limit:
|
||||
resource.setrlimit(resource.RLIMIT_AS, (soft_limit, hard_limit))
|
||||
logging.debug(f"Changed AS mem limit {old_limit} -> {soft_limit}")
|
||||
del resource, soft_limit, hard_limit
|
||||
|
||||
pony_config = config["PONY"]
|
||||
db.bind(**pony_config)
|
||||
db.generate_mapping()
|
||||
|
||||
@@ -105,8 +120,8 @@ def autogen(config: dict):
|
||||
try:
|
||||
with Locker("autogen"):
|
||||
|
||||
with multiprocessing.Pool(config["GENERATORS"], initializer=init_db,
|
||||
initargs=(config["PONY"],), maxtasksperchild=10) as generator_pool:
|
||||
with multiprocessing.Pool(config["GENERATORS"], initializer=init_generator,
|
||||
initargs=(config,), maxtasksperchild=10) as generator_pool:
|
||||
with db_session:
|
||||
to_start = select(generation for generation in Generation if generation.state == STATE_STARTED)
|
||||
|
||||
|
||||
@@ -105,8 +105,9 @@ def roll_options(options: Dict[str, Union[dict, str]],
|
||||
plando_options=plando_options)
|
||||
else:
|
||||
for i, yaml_data in enumerate(yaml_datas):
|
||||
rolled_results[f"{filename}/{i + 1}"] = roll_settings(yaml_data,
|
||||
plando_options=plando_options)
|
||||
if yaml_data is not None:
|
||||
rolled_results[f"{filename}/{i + 1}"] = roll_settings(yaml_data,
|
||||
plando_options=plando_options)
|
||||
except Exception as e:
|
||||
if e.__cause__:
|
||||
results[filename] = f"Failed to generate options in {filename}: {e} - {e.__cause__}"
|
||||
|
||||
@@ -18,13 +18,6 @@ def get_world_theme(game_name: str):
|
||||
return 'grass'
|
||||
|
||||
|
||||
@app.before_request
|
||||
def register_session():
|
||||
session.permanent = True # technically 31 days after the last visit
|
||||
if not session.get("_id", None):
|
||||
session["_id"] = uuid4() # uniquely identify each session without needing a login
|
||||
|
||||
|
||||
@app.errorhandler(404)
|
||||
@app.errorhandler(jinja2.exceptions.TemplateNotFound)
|
||||
def page_not_found(err):
|
||||
|
||||
31
WebHostLib/session.py
Normal file
31
WebHostLib/session.py
Normal file
@@ -0,0 +1,31 @@
|
||||
from uuid import uuid4, UUID
|
||||
|
||||
from flask import session, render_template
|
||||
|
||||
from WebHostLib import app
|
||||
|
||||
|
||||
@app.before_request
|
||||
def register_session():
|
||||
session.permanent = True # technically 31 days after the last visit
|
||||
if not session.get("_id", None):
|
||||
session["_id"] = uuid4() # uniquely identify each session without needing a login
|
||||
|
||||
|
||||
@app.route('/session')
|
||||
def show_session():
|
||||
return render_template(
|
||||
"session.html",
|
||||
)
|
||||
|
||||
|
||||
@app.route('/session/<string:_id>')
|
||||
def set_session(_id: str):
|
||||
new_id: UUID = UUID(_id, version=4)
|
||||
old_id: UUID = session["_id"]
|
||||
if old_id != new_id:
|
||||
session["_id"] = new_id
|
||||
return render_template(
|
||||
"session.html",
|
||||
old_id=old_id,
|
||||
)
|
||||
@@ -178,8 +178,15 @@
|
||||
})
|
||||
.then(text => new DOMParser().parseFromString(text, 'text/html'))
|
||||
.then(newDocument => {
|
||||
let el = newDocument.getElementById("host-room-info");
|
||||
document.getElementById("host-room-info").innerHTML = el.innerHTML;
|
||||
["host-room-info", "slots-table"].forEach(function(id) {
|
||||
const newEl = newDocument.getElementById(id);
|
||||
const oldEl = document.getElementById(id);
|
||||
if (oldEl && newEl) {
|
||||
oldEl.innerHTML = newEl.innerHTML;
|
||||
} else if (newEl) {
|
||||
console.warn(`Did not find element to replace for ${id}`)
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
|
||||
@@ -8,7 +8,7 @@
|
||||
{%- endmacro %}
|
||||
{% macro list_patches_room(room) %}
|
||||
{% if room.seed.slots %}
|
||||
<table>
|
||||
<table id="slots-table">
|
||||
<thead>
|
||||
<tr>
|
||||
<th>Id</th>
|
||||
|
||||
30
WebHostLib/templates/session.html
Normal file
30
WebHostLib/templates/session.html
Normal file
@@ -0,0 +1,30 @@
|
||||
{% extends 'pageWrapper.html' %}
|
||||
|
||||
{% block head %}
|
||||
{% include 'header/stoneHeader.html' %}
|
||||
<title>Session</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
<div class="markdown">
|
||||
{% if old_id is defined %}
|
||||
<p>Your old code was:</p>
|
||||
<code>{{ old_id }}</code>
|
||||
<br>
|
||||
{% endif %}
|
||||
<p>The following code is your unique identifier, it binds your uploaded content, such as rooms and seeds to you.
|
||||
Treat it like a combined login name and password.
|
||||
You should save this securely if you ever need to restore access.
|
||||
You can also paste it into another device to access your content from multiple devices / browsers.
|
||||
Some browsers, such as Brave, will delete your identifier cookie on a timer.</p>
|
||||
<code>{{ session["_id"] }}</code>
|
||||
<br>
|
||||
<p>
|
||||
The following link can be used to set the identifier. Do not share the code or link with others. <br>
|
||||
<a href="{{ url_for('set_session', _id=session['_id']) }}">
|
||||
{{ url_for('set_session', _id=session['_id'], _external=True) }}
|
||||
</a>
|
||||
</p>
|
||||
</div>
|
||||
{% endblock %}
|
||||
@@ -26,6 +26,7 @@
|
||||
<li><a href="/user-content">User Content</a></li>
|
||||
<li><a href="{{url_for('stats')}}">Game Statistics</a></li>
|
||||
<li><a href="/glossary/en">Glossary</a></li>
|
||||
<li><a href="{{url_for("show_session")}}">Session / Login</a></li>
|
||||
</ul>
|
||||
|
||||
<h2>Tutorials</h2>
|
||||
|
||||
@@ -4,9 +4,6 @@
|
||||
{% include 'header/grassHeader.html' %}
|
||||
<title>Option Templates (YAML)</title>
|
||||
<link rel="stylesheet" type="text/css" href="{{ url_for('static', filename="styles/markdown.css") }}" />
|
||||
<script src="https://cdnjs.cloudflare.com/ajax/libs/showdown/1.9.1/showdown.min.js"
|
||||
integrity="sha512-L03kznCrNOfVxOUovR6ESfCz9Gfny7gihUX/huVbQB9zjODtYpxaVtIaAkpetoiyV2eqWbvxMH9fiSv5enX7bw=="
|
||||
crossorigin="anonymous"></script>
|
||||
{% endblock %}
|
||||
|
||||
{% block body %}
|
||||
|
||||
@@ -69,6 +69,14 @@ cdef struct IndexEntry:
|
||||
size_t count
|
||||
|
||||
|
||||
if TYPE_CHECKING:
|
||||
State = Dict[Tuple[int, int], Set[int]]
|
||||
else:
|
||||
State = Union[Tuple[int, int], Set[int], defaultdict]
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
|
||||
@cython.auto_pickle(False)
|
||||
cdef class LocationStore:
|
||||
"""Compact store for locations and their items in a MultiServer"""
|
||||
@@ -137,10 +145,16 @@ cdef class LocationStore:
|
||||
warnings.warn("Game has no locations")
|
||||
|
||||
# allocate the arrays and invalidate index (0xff...)
|
||||
self.entries = <LocationEntry*>self._mem.alloc(count, sizeof(LocationEntry))
|
||||
if count:
|
||||
# leaving entries as NULL if there are none, makes potential memory errors more visible
|
||||
self.entries = <LocationEntry*>self._mem.alloc(count, sizeof(LocationEntry))
|
||||
self.sender_index = <IndexEntry*>self._mem.alloc(max_sender + 1, sizeof(IndexEntry))
|
||||
self._raw_proxies = <PyObject**>self._mem.alloc(max_sender + 1, sizeof(PyObject*))
|
||||
|
||||
assert (not self.entries) == (not count)
|
||||
assert self.sender_index
|
||||
assert self._raw_proxies
|
||||
|
||||
# build entries and index
|
||||
cdef size_t i = 0
|
||||
for sender, locations in sorted(locations_dict.items()):
|
||||
@@ -190,8 +204,6 @@ cdef class LocationStore:
|
||||
raise KeyError(key)
|
||||
return <object>self._raw_proxies[key]
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
def get(self, key: int, default: T) -> Union[PlayerLocationProxy, T]:
|
||||
# calling into self.__getitem__ here is slow, but this is not used in MultiServer
|
||||
try:
|
||||
@@ -246,12 +258,11 @@ cdef class LocationStore:
|
||||
all_locations[sender].add(entry.location)
|
||||
return all_locations
|
||||
|
||||
if TYPE_CHECKING:
|
||||
State = Dict[Tuple[int, int], Set[int]]
|
||||
else:
|
||||
State = Union[Tuple[int, int], Set[int], defaultdict]
|
||||
|
||||
def get_checked(self, state: State, team: int, slot: int) -> List[int]:
|
||||
cdef ap_player_t sender = slot
|
||||
if sender < 0 or sender >= self.sender_index_size:
|
||||
raise KeyError(slot)
|
||||
|
||||
# This used to validate checks actually exist. A remnant from the past.
|
||||
# If the order of locations becomes relevant at some point, we could not do sorted(set), so leaving it.
|
||||
cdef set checked = state[team, slot]
|
||||
@@ -263,7 +274,6 @@ cdef class LocationStore:
|
||||
|
||||
# Unless the set is close to empty, it's cheaper to use the python set directly, so we do that.
|
||||
cdef LocationEntry* entry
|
||||
cdef ap_player_t sender = slot
|
||||
cdef size_t start = self.sender_index[sender].start
|
||||
cdef size_t count = self.sender_index[sender].count
|
||||
return [entry.location for
|
||||
@@ -273,9 +283,11 @@ cdef class LocationStore:
|
||||
def get_missing(self, state: State, team: int, slot: int) -> List[int]:
|
||||
cdef LocationEntry* entry
|
||||
cdef ap_player_t sender = slot
|
||||
if sender < 0 or sender >= self.sender_index_size:
|
||||
raise KeyError(slot)
|
||||
cdef set checked = state[team, slot]
|
||||
cdef size_t start = self.sender_index[sender].start
|
||||
cdef size_t count = self.sender_index[sender].count
|
||||
cdef set checked = state[team, slot]
|
||||
if not len(checked):
|
||||
# Skip `in` if none have been checked.
|
||||
# This optimizes the case where everyone connects to a fresh game at the same time.
|
||||
@@ -290,9 +302,11 @@ cdef class LocationStore:
|
||||
def get_remaining(self, state: State, team: int, slot: int) -> List[Tuple[int, int]]:
|
||||
cdef LocationEntry* entry
|
||||
cdef ap_player_t sender = slot
|
||||
if sender < 0 or sender >= self.sender_index_size:
|
||||
raise KeyError(slot)
|
||||
cdef set checked = state[team, slot]
|
||||
cdef size_t start = self.sender_index[sender].start
|
||||
cdef size_t count = self.sender_index[sender].count
|
||||
cdef set checked = state[team, slot]
|
||||
return sorted([(entry.receiver, entry.item) for
|
||||
entry in self.entries[start:start+count] if
|
||||
entry.location not in checked])
|
||||
@@ -328,7 +342,8 @@ cdef class PlayerLocationProxy:
|
||||
cdef LocationEntry* entry = NULL
|
||||
# binary search
|
||||
cdef size_t l = self._store.sender_index[self._player].start
|
||||
cdef size_t r = l + self._store.sender_index[self._player].count
|
||||
cdef size_t e = l + self._store.sender_index[self._player].count
|
||||
cdef size_t r = e
|
||||
cdef size_t m
|
||||
while l < r:
|
||||
m = (l + r) // 2
|
||||
@@ -337,7 +352,7 @@ cdef class PlayerLocationProxy:
|
||||
l = m + 1
|
||||
else:
|
||||
r = m
|
||||
if entry: # count != 0
|
||||
if l < e:
|
||||
entry = self._store.entries + l
|
||||
if entry.location == loc:
|
||||
return entry
|
||||
@@ -349,8 +364,6 @@ cdef class PlayerLocationProxy:
|
||||
return entry.item, entry.receiver, entry.flags
|
||||
raise KeyError(f"No location {key} for player {self._player}")
|
||||
|
||||
T = TypeVar('T')
|
||||
|
||||
def get(self, key: int, default: T) -> Union[Tuple[int, int, int], T]:
|
||||
cdef LocationEntry* entry = self._get(key)
|
||||
if entry:
|
||||
|
||||
@@ -3,8 +3,16 @@ import os
|
||||
|
||||
def make_ext(modname, pyxfilename):
|
||||
from distutils.extension import Extension
|
||||
return Extension(name=modname,
|
||||
sources=[pyxfilename],
|
||||
depends=["intset.h"],
|
||||
include_dirs=[os.getcwd()],
|
||||
language="c")
|
||||
return Extension(
|
||||
name=modname,
|
||||
sources=[pyxfilename],
|
||||
depends=["intset.h"],
|
||||
include_dirs=[os.getcwd()],
|
||||
language="c",
|
||||
# to enable ASAN and debug build:
|
||||
# extra_compile_args=["-fsanitize=address", "-UNDEBUG", "-Og", "-g"],
|
||||
# extra_objects=["-fsanitize=address"],
|
||||
# NOTE: we can not put -lasan at the front of link args, so needs to be run with
|
||||
# LD_PRELOAD=/usr/lib/libasan.so ASAN_OPTIONS=detect_leaks=0 path/to/exe
|
||||
# NOTE: this can't find everything unless libpython and cymem are also built with ASAN
|
||||
)
|
||||
|
||||
@@ -36,6 +36,9 @@
|
||||
# Castlevania 64
|
||||
/worlds/cv64/ @LiquidCat64
|
||||
|
||||
# Castlevania: Circle of the Moon
|
||||
/worlds/cvcotm/ @LiquidCat64
|
||||
|
||||
# Celeste 64
|
||||
/worlds/celeste64/ @PoryGone
|
||||
|
||||
@@ -142,6 +145,9 @@
|
||||
# Risk of Rain 2
|
||||
/worlds/ror2/ @kindasneaki
|
||||
|
||||
# Saving Princess
|
||||
/worlds/saving_princess/ @LeonarthCG
|
||||
|
||||
# Shivers
|
||||
/worlds/shivers/ @GodlFire
|
||||
|
||||
|
||||
@@ -351,7 +351,7 @@ Sent to the server to update the status of a Hint. The client must be the 'recei
|
||||
| ---- | ---- | ----- |
|
||||
| player | int | The ID of the player whose location is being hinted for. |
|
||||
| location | int | The ID of the location to update the hint for. If no hint exists for this location, the packet is ignored. |
|
||||
| status | [HintStatus](#HintStatus) | Optional. If included, sets the status of the hint to this status. |
|
||||
| status | [HintStatus](#HintStatus) | Optional. If included, sets the status of the hint to this status. Cannot set `HINT_FOUND`, or change the status from `HINT_FOUND`. |
|
||||
|
||||
#### HintStatus
|
||||
An enumeration containing the possible hint states.
|
||||
@@ -359,12 +359,16 @@ An enumeration containing the possible hint states.
|
||||
```python
|
||||
import enum
|
||||
class HintStatus(enum.IntEnum):
|
||||
HINT_FOUND = 0
|
||||
HINT_UNSPECIFIED = 1
|
||||
HINT_NO_PRIORITY = 10
|
||||
HINT_AVOID = 20
|
||||
HINT_PRIORITY = 30
|
||||
HINT_FOUND = 0 # The location has been collected. Status cannot be changed once found.
|
||||
HINT_UNSPECIFIED = 1 # The receiving player has not specified any status
|
||||
HINT_NO_PRIORITY = 10 # The receiving player has specified that the item is unneeded
|
||||
HINT_AVOID = 20 # The receiving player has specified that the item is detrimental
|
||||
HINT_PRIORITY = 30 # The receiving player has specified that the item is needed
|
||||
```
|
||||
- Hints for items with `ItemClassification.trap` default to `HINT_AVOID`.
|
||||
- Hints created with `LocationScouts`, `!hint_location`, or similar (hinting a location) default to `HINT_UNSPECIFIED`.
|
||||
- Hints created with `!hint` or similar (hinting an item for yourself) default to `HINT_PRIORITY`.
|
||||
- Once a hint is collected, its' status is updated to `HINT_FOUND` automatically, and can no longer be changed.
|
||||
|
||||
### StatusUpdate
|
||||
Sent to the server to update on the sender's status. Examples include readiness or goal completion. (Example: defeated Ganon in A Link to the Past)
|
||||
@@ -668,6 +672,7 @@ class Hint(typing.NamedTuple):
|
||||
found: bool
|
||||
entrance: str = ""
|
||||
item_flags: int = 0
|
||||
status: HintStatus = HintStatus.HINT_UNSPECIFIED
|
||||
```
|
||||
|
||||
### Data Package Contents
|
||||
|
||||
@@ -7,7 +7,9 @@ use that version. These steps are for developers or platforms without compiled r
|
||||
## General
|
||||
|
||||
What you'll need:
|
||||
* [Python 3.10.15 or newer](https://www.python.org/downloads/), not the Windows Store version
|
||||
* [Python 3.10.11 or newer](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.12.x is currently the newest supported version
|
||||
* pip: included in downloads from python.org, separate in many Linux distributions
|
||||
* Matching C compiler
|
||||
|
||||
@@ -27,8 +27,14 @@
|
||||
# If you wish to deploy, uncomment the following line and set it to something not easily guessable.
|
||||
# SECRET_KEY: "Your secret key here"
|
||||
|
||||
# TODO
|
||||
#JOB_THRESHOLD: 2
|
||||
# Slot limit to post a generation to Generator process pool instead of rolling directly in WebHost process
|
||||
#JOB_THRESHOLD: 1
|
||||
|
||||
# After what time in seconds should generation be aborted, freeing the queue slot. Can be set to None to disable.
|
||||
#JOB_TIME: 600
|
||||
|
||||
# Memory limit for Generator processes in bytes, -1 for unlimited. Currently only works on Linux.
|
||||
#GENERATOR_MEMORY_LIMIT: 4294967296
|
||||
|
||||
# waitress uses one thread for I/O, these are for processing of view that get sent
|
||||
#WAITRESS_THREADS: 10
|
||||
|
||||
@@ -186,6 +186,11 @@ Root: HKCR; Subkey: "{#MyAppName}cv64patch"; ValueData: "Arc
|
||||
Root: HKCR; Subkey: "{#MyAppName}cv64patch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}cv64patch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".apcvcotm"; ValueData: "{#MyAppName}cvcotmpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}cvcotmpatch"; ValueData: "Archipelago Castlevania Circle of the Moon Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}cvcotmpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}cvcotmpatch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".apmm2"; ValueData: "{#MyAppName}mm2patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}mm2patch"; ValueData: "Archipelago Mega Man 2 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}mm2patch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
|
||||
|
||||
42
kvui.py
42
kvui.py
@@ -3,6 +3,8 @@ import logging
|
||||
import sys
|
||||
import typing
|
||||
import re
|
||||
import io
|
||||
import pkgutil
|
||||
from collections import deque
|
||||
|
||||
assert "kivy" not in sys.modules, "kvui should be imported before kivy for frozen compatibility"
|
||||
@@ -34,6 +36,7 @@ from kivy.app import App
|
||||
from kivy.core.window import Window
|
||||
from kivy.core.clipboard import Clipboard
|
||||
from kivy.core.text.markup import MarkupLabel
|
||||
from kivy.core.image import ImageLoader, ImageLoaderBase, ImageData
|
||||
from kivy.base import ExceptionHandler, ExceptionManager
|
||||
from kivy.clock import Clock
|
||||
from kivy.factory import Factory
|
||||
@@ -61,6 +64,7 @@ from kivy.uix.recycleboxlayout import RecycleBoxLayout
|
||||
from kivy.uix.recycleview.layout import LayoutSelectionBehavior
|
||||
from kivy.animation import Animation
|
||||
from kivy.uix.popup import Popup
|
||||
from kivy.uix.image import AsyncImage
|
||||
|
||||
fade_in_animation = Animation(opacity=0, duration=0) + Animation(opacity=1, duration=0.25)
|
||||
|
||||
@@ -367,7 +371,7 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
||||
if self.hint["status"] == HintStatus.HINT_FOUND:
|
||||
return
|
||||
ctx = App.get_running_app().ctx
|
||||
if ctx.slot == self.hint["receiving_player"]: # If this player owns this hint
|
||||
if ctx.slot_concerns_self(self.hint["receiving_player"]): # If this player owns this hint
|
||||
# open a dropdown
|
||||
self.dropdown.open(self.ids["status"])
|
||||
elif self.selected:
|
||||
@@ -796,7 +800,7 @@ class HintLog(RecycleView):
|
||||
hint_status_node = self.parser.handle_node({"type": "color",
|
||||
"color": status_colors.get(hint["status"], "red"),
|
||||
"text": status_names.get(hint["status"], "Unknown")})
|
||||
if hint["status"] != HintStatus.HINT_FOUND and hint["receiving_player"] == ctx.slot:
|
||||
if hint["status"] != HintStatus.HINT_FOUND and ctx.slot_concerns_self(hint["receiving_player"]):
|
||||
hint_status_node = f"[u]{hint_status_node}[/u]"
|
||||
data.append({
|
||||
"receiving": {"text": self.parser.handle_node({"type": "player_id", "text": hint["receiving_player"]})},
|
||||
@@ -838,6 +842,40 @@ class HintLog(RecycleView):
|
||||
element.height = max_height
|
||||
|
||||
|
||||
class ApAsyncImage(AsyncImage):
|
||||
def is_uri(self, filename: str) -> bool:
|
||||
if filename.startswith("ap:"):
|
||||
return True
|
||||
else:
|
||||
return super().is_uri(filename)
|
||||
|
||||
|
||||
class ImageLoaderPkgutil(ImageLoaderBase):
|
||||
def load(self, filename: str) -> typing.List[ImageData]:
|
||||
# take off the "ap:" prefix
|
||||
module, path = filename[3:].split("/", 1)
|
||||
data = pkgutil.get_data(module, path)
|
||||
return self._bytes_to_data(data)
|
||||
|
||||
def _bytes_to_data(self, data: typing.Union[bytes, bytearray]) -> typing.List[ImageData]:
|
||||
loader = next(loader for loader in ImageLoader.loaders if loader.can_load_memory())
|
||||
return loader.load(loader, io.BytesIO(data))
|
||||
|
||||
|
||||
# grab the default loader method so we can override it but use it as a fallback
|
||||
_original_image_loader_load = ImageLoader.load
|
||||
|
||||
|
||||
def load_override(filename: str, default_load=_original_image_loader_load, **kwargs):
|
||||
if filename.startswith("ap:"):
|
||||
return ImageLoaderPkgutil(filename)
|
||||
else:
|
||||
return default_load(filename, **kwargs)
|
||||
|
||||
|
||||
ImageLoader.load = load_override
|
||||
|
||||
|
||||
class E(ExceptionHandler):
|
||||
logger = logging.getLogger("Client")
|
||||
|
||||
|
||||
@@ -599,6 +599,7 @@ class ServerOptions(Group):
|
||||
savefile: Optional[str] = None
|
||||
disable_save: bool = False
|
||||
loglevel: str = "info"
|
||||
logtime: bool = False
|
||||
server_password: Optional[ServerPassword] = None
|
||||
disable_item_cheat: Union[DisableItemCheat, bool] = False
|
||||
location_check_points: LocationCheckPoints = LocationCheckPoints(1)
|
||||
|
||||
@@ -115,6 +115,7 @@ class Base:
|
||||
def test_get_for_player(self) -> None:
|
||||
self.assertEqual(self.store.get_for_player(3), {4: {9}})
|
||||
self.assertEqual(self.store.get_for_player(1), {1: {13}, 2: {22, 23}})
|
||||
self.assertEqual(self.store.get_for_player(9999), {})
|
||||
|
||||
def test_get_checked(self) -> None:
|
||||
self.assertEqual(self.store.get_checked(full_state, 0, 1), [11, 12, 13])
|
||||
@@ -122,18 +123,48 @@ class Base:
|
||||
self.assertEqual(self.store.get_checked(empty_state, 0, 1), [])
|
||||
self.assertEqual(self.store.get_checked(full_state, 0, 3), [9])
|
||||
|
||||
def test_get_checked_exception(self) -> None:
|
||||
with self.assertRaises(KeyError):
|
||||
self.store.get_checked(empty_state, 0, 9999)
|
||||
bad_state = {(0, 6): {1}}
|
||||
with self.assertRaises(KeyError):
|
||||
self.store.get_checked(bad_state, 0, 6)
|
||||
bad_state = {(0, 9999): set()}
|
||||
with self.assertRaises(KeyError):
|
||||
self.store.get_checked(bad_state, 0, 9999)
|
||||
|
||||
def test_get_missing(self) -> None:
|
||||
self.assertEqual(self.store.get_missing(full_state, 0, 1), [])
|
||||
self.assertEqual(self.store.get_missing(one_state, 0, 1), [11, 13])
|
||||
self.assertEqual(self.store.get_missing(empty_state, 0, 1), [11, 12, 13])
|
||||
self.assertEqual(self.store.get_missing(empty_state, 0, 3), [9])
|
||||
|
||||
def test_get_missing_exception(self) -> None:
|
||||
with self.assertRaises(KeyError):
|
||||
self.store.get_missing(empty_state, 0, 9999)
|
||||
bad_state = {(0, 6): {1}}
|
||||
with self.assertRaises(KeyError):
|
||||
self.store.get_missing(bad_state, 0, 6)
|
||||
bad_state = {(0, 9999): set()}
|
||||
with self.assertRaises(KeyError):
|
||||
self.store.get_missing(bad_state, 0, 9999)
|
||||
|
||||
def test_get_remaining(self) -> None:
|
||||
self.assertEqual(self.store.get_remaining(full_state, 0, 1), [])
|
||||
self.assertEqual(self.store.get_remaining(one_state, 0, 1), [(1, 13), (2, 21)])
|
||||
self.assertEqual(self.store.get_remaining(empty_state, 0, 1), [(1, 13), (2, 21), (2, 22)])
|
||||
self.assertEqual(self.store.get_remaining(empty_state, 0, 3), [(4, 99)])
|
||||
|
||||
def test_get_remaining_exception(self) -> None:
|
||||
with self.assertRaises(KeyError):
|
||||
self.store.get_remaining(empty_state, 0, 9999)
|
||||
bad_state = {(0, 6): {1}}
|
||||
with self.assertRaises(KeyError):
|
||||
self.store.get_missing(bad_state, 0, 6)
|
||||
bad_state = {(0, 9999): set()}
|
||||
with self.assertRaises(KeyError):
|
||||
self.store.get_remaining(bad_state, 0, 9999)
|
||||
|
||||
def test_location_set_intersection(self) -> None:
|
||||
locations = {10, 11, 12}
|
||||
locations.intersection_update(self.store[1])
|
||||
@@ -181,6 +212,16 @@ class Base:
|
||||
})
|
||||
self.assertEqual(len(store), 1)
|
||||
self.assertEqual(len(store[1]), 0)
|
||||
self.assertEqual(sorted(store.find_item(set(), 1)), [])
|
||||
self.assertEqual(sorted(store.find_item({1}, 1)), [])
|
||||
self.assertEqual(sorted(store.find_item({1, 2}, 1)), [])
|
||||
self.assertEqual(store.get_for_player(1), {})
|
||||
self.assertEqual(store.get_checked(empty_state, 0, 1), [])
|
||||
self.assertEqual(store.get_checked(full_state, 0, 1), [])
|
||||
self.assertEqual(store.get_missing(empty_state, 0, 1), [])
|
||||
self.assertEqual(store.get_missing(full_state, 0, 1), [])
|
||||
self.assertEqual(store.get_remaining(empty_state, 0, 1), [])
|
||||
self.assertEqual(store.get_remaining(full_state, 0, 1), [])
|
||||
|
||||
def test_no_locations_for_1(self) -> None:
|
||||
store = self.type({
|
||||
|
||||
@@ -207,6 +207,7 @@ components: List[Component] = [
|
||||
]
|
||||
|
||||
|
||||
# if registering an icon from within an apworld, the format "ap:module.name/path/to/file.png" can be used
|
||||
icon_paths = {
|
||||
'icon': local_path('data', 'icon.png'),
|
||||
'mcicon': local_path('data', 'mcicon.png'),
|
||||
|
||||
@@ -47,8 +47,6 @@ class LocationData:
|
||||
self.local_item: int = None
|
||||
|
||||
def get_random_position(self, random):
|
||||
x: int = None
|
||||
y: int = None
|
||||
if self.world_positions is None or len(self.world_positions) == 0:
|
||||
if self.room_id is None:
|
||||
return None
|
||||
|
||||
@@ -76,10 +76,9 @@ def create_regions(options: PerGameCommonOptions, multiworld: MultiWorld, player
|
||||
multiworld.regions.append(credits_room_far_side)
|
||||
|
||||
dragon_slay_check = options.dragon_slay_check.value
|
||||
priority_locations = determine_priority_locations(multiworld, dragon_slay_check)
|
||||
priority_locations = determine_priority_locations()
|
||||
|
||||
for name, location_data in location_table.items():
|
||||
require_sword = False
|
||||
if location_data.region == "Varies":
|
||||
if location_data.name == "Slay Yorgle":
|
||||
if not dragon_slay_check:
|
||||
@@ -154,6 +153,7 @@ def create_regions(options: PerGameCommonOptions, multiworld: MultiWorld, player
|
||||
|
||||
|
||||
# Placeholder for adding sets of priority locations at generation, possibly as an option in the future
|
||||
def determine_priority_locations(world: MultiWorld, dragon_slay_check: bool) -> {}:
|
||||
# def determine_priority_locations(multiworld: MultiWorld, dragon_slay_check: bool) -> {}:
|
||||
def determine_priority_locations() -> {}:
|
||||
priority_locations = {}
|
||||
return priority_locations
|
||||
|
||||
@@ -86,9 +86,7 @@ class AdventureDeltaPatch(APPatch, metaclass=AutoPatchRegister):
|
||||
|
||||
# locations: [], autocollect: [], seed_name: bytes,
|
||||
def __init__(self, *args: Any, **kwargs: Any) -> None:
|
||||
patch_only = True
|
||||
if "autocollect" in kwargs:
|
||||
patch_only = False
|
||||
self.foreign_items: [AdventureForeignItemInfo] = [AdventureForeignItemInfo(loc.short_location_id, loc.room_id, loc.room_x, loc.room_y)
|
||||
for loc in kwargs["locations"]]
|
||||
|
||||
|
||||
@@ -446,7 +446,7 @@ class AdventureWorld(World):
|
||||
# end of ordered Main.py calls
|
||||
|
||||
def create_item(self, name: str) -> Item:
|
||||
item_data: ItemData = item_table.get(name)
|
||||
item_data: ItemData = item_table[name]
|
||||
return AdventureItem(name, item_data.classification, item_data.id, self.player)
|
||||
|
||||
def create_event(self, name: str, classification: ItemClassification) -> Item:
|
||||
|
||||
@@ -59,156 +59,316 @@ class ItemData:
|
||||
type: ItemType
|
||||
group: ItemGroup
|
||||
|
||||
def __init__(self, id: int, count: int, type: ItemType, group: ItemGroup):
|
||||
def __init__(self, aId: int, count: int, aType: ItemType, group: ItemGroup):
|
||||
"""
|
||||
Initialisation of the item data
|
||||
@param id: The item ID
|
||||
@param aId: The item ID
|
||||
@param count: the number of items in the pool
|
||||
@param type: the importance type of the item
|
||||
@param aType: the importance type of the item
|
||||
@param group: the usage of the item in the game
|
||||
"""
|
||||
self.id = id
|
||||
self.id = aId
|
||||
self.count = count
|
||||
self.type = type
|
||||
self.type = aType
|
||||
self.group = group
|
||||
|
||||
class ItemNames:
|
||||
"""
|
||||
Constants used to represent the mane of every items.
|
||||
"""
|
||||
# Normal items
|
||||
ANEMONE = "Anemone"
|
||||
ARNASSI_STATUE = "Arnassi Statue"
|
||||
BIG_SEED = "Big Seed"
|
||||
GLOWING_SEED = "Glowing Seed"
|
||||
BLACK_PEARL = "Black Pearl"
|
||||
BABY_BLASTER = "Baby Blaster"
|
||||
CRAB_ARMOR = "Crab Armor"
|
||||
BABY_DUMBO = "Baby Dumbo"
|
||||
TOOTH = "Tooth"
|
||||
ENERGY_STATUE = "Energy Statue"
|
||||
KROTITE_ARMOR = "Krotite Armor"
|
||||
GOLDEN_STARFISH = "Golden Starfish"
|
||||
GOLDEN_GEAR = "Golden Gear"
|
||||
JELLY_BEACON = "Jelly Beacon"
|
||||
JELLY_COSTUME = "Jelly Costume"
|
||||
JELLY_PLANT = "Jelly Plant"
|
||||
MITHALAS_DOLL = "Mithalas Doll"
|
||||
MITHALAN_DRESS = "Mithalan Dress"
|
||||
MITHALAS_BANNER = "Mithalas Banner"
|
||||
MITHALAS_POT = "Mithalas Pot"
|
||||
MUTANT_COSTUME = "Mutant Costume"
|
||||
BABY_NAUTILUS = "Baby Nautilus"
|
||||
BABY_PIRANHA = "Baby Piranha"
|
||||
ARNASSI_ARMOR = "Arnassi Armor"
|
||||
SEED_BAG = "Seed Bag"
|
||||
KING_S_SKULL = "King's Skull"
|
||||
SONG_PLANT_SPORE = "Song Plant Spore"
|
||||
STONE_HEAD = "Stone Head"
|
||||
SUN_KEY = "Sun Key"
|
||||
GIRL_COSTUME = "Girl Costume"
|
||||
ODD_CONTAINER = "Odd Container"
|
||||
TRIDENT = "Trident"
|
||||
TURTLE_EGG = "Turtle Egg"
|
||||
JELLY_EGG = "Jelly Egg"
|
||||
URCHIN_COSTUME = "Urchin Costume"
|
||||
BABY_WALKER = "Baby Walker"
|
||||
VEDHA_S_CURE_ALL = "Vedha's Cure-All"
|
||||
ZUUNA_S_PEROGI = "Zuuna's Perogi"
|
||||
ARCANE_POULTICE = "Arcane Poultice"
|
||||
BERRY_ICE_CREAM = "Berry Ice Cream"
|
||||
BUTTERY_SEA_LOAF = "Buttery Sea Loaf"
|
||||
COLD_BORSCHT = "Cold Borscht"
|
||||
COLD_SOUP = "Cold Soup"
|
||||
CRAB_CAKE = "Crab Cake"
|
||||
DIVINE_SOUP = "Divine Soup"
|
||||
DUMBO_ICE_CREAM = "Dumbo Ice Cream"
|
||||
FISH_OIL = "Fish Oil"
|
||||
GLOWING_EGG = "Glowing Egg"
|
||||
HAND_ROLL = "Hand Roll"
|
||||
HEALING_POULTICE = "Healing Poultice"
|
||||
HEARTY_SOUP = "Hearty Soup"
|
||||
HOT_BORSCHT = "Hot Borscht"
|
||||
HOT_SOUP = "Hot Soup"
|
||||
ICE_CREAM = "Ice Cream"
|
||||
LEADERSHIP_ROLL = "Leadership Roll"
|
||||
LEAF_POULTICE = "Leaf Poultice"
|
||||
LEECHING_POULTICE = "Leeching Poultice"
|
||||
LEGENDARY_CAKE = "Legendary Cake"
|
||||
LOAF_OF_LIFE = "Loaf of Life"
|
||||
LONG_LIFE_SOUP = "Long Life Soup"
|
||||
MAGIC_SOUP = "Magic Soup"
|
||||
MUSHROOM_X_2 = "Mushroom x 2"
|
||||
PEROGI = "Perogi"
|
||||
PLANT_LEAF = "Plant Leaf"
|
||||
PLUMP_PEROGI = "Plump Perogi"
|
||||
POISON_LOAF = "Poison Loaf"
|
||||
POISON_SOUP = "Poison Soup"
|
||||
RAINBOW_MUSHROOM = "Rainbow Mushroom"
|
||||
RAINBOW_SOUP = "Rainbow Soup"
|
||||
RED_BERRY = "Red Berry"
|
||||
RED_BULB_X_2 = "Red Bulb x 2"
|
||||
ROTTEN_CAKE = "Rotten Cake"
|
||||
ROTTEN_LOAF_X_8 = "Rotten Loaf x 8"
|
||||
ROTTEN_MEAT = "Rotten Meat"
|
||||
ROYAL_SOUP = "Royal Soup"
|
||||
SEA_CAKE = "Sea Cake"
|
||||
SEA_LOAF = "Sea Loaf"
|
||||
SHARK_FIN_SOUP = "Shark Fin Soup"
|
||||
SIGHT_POULTICE = "Sight Poultice"
|
||||
SMALL_BONE_X_2 = "Small Bone x 2"
|
||||
SMALL_EGG = "Small Egg"
|
||||
SMALL_TENTACLE_X_2 = "Small Tentacle x 2"
|
||||
SPECIAL_BULB = "Special Bulb"
|
||||
SPECIAL_CAKE = "Special Cake"
|
||||
SPICY_MEAT_X_2 = "Spicy Meat x 2"
|
||||
SPICY_ROLL = "Spicy Roll"
|
||||
SPICY_SOUP = "Spicy Soup"
|
||||
SPIDER_ROLL = "Spider Roll"
|
||||
SWAMP_CAKE = "Swamp Cake"
|
||||
TASTY_CAKE = "Tasty Cake"
|
||||
TASTY_ROLL = "Tasty Roll"
|
||||
TOUGH_CAKE = "Tough Cake"
|
||||
TURTLE_SOUP = "Turtle Soup"
|
||||
VEDHA_SEA_CRISP = "Vedha Sea Crisp"
|
||||
VEGGIE_CAKE = "Veggie Cake"
|
||||
VEGGIE_ICE_CREAM = "Veggie Ice Cream"
|
||||
VEGGIE_SOUP = "Veggie Soup"
|
||||
VOLCANO_ROLL = "Volcano Roll"
|
||||
HEALTH_UPGRADE = "Health Upgrade"
|
||||
WOK = "Wok"
|
||||
EEL_OIL_X_2 = "Eel Oil x 2"
|
||||
FISH_MEAT_X_2 = "Fish Meat x 2"
|
||||
FISH_OIL_X_3 = "Fish Oil x 3"
|
||||
GLOWING_EGG_X_2 = "Glowing Egg x 2"
|
||||
HEALING_POULTICE_X_2 = "Healing Poultice x 2"
|
||||
HOT_SOUP_X_2 = "Hot Soup x 2"
|
||||
LEADERSHIP_ROLL_X_2 = "Leadership Roll x 2"
|
||||
LEAF_POULTICE_X_3 = "Leaf Poultice x 3"
|
||||
PLANT_LEAF_X_2 = "Plant Leaf x 2"
|
||||
PLANT_LEAF_X_3 = "Plant Leaf x 3"
|
||||
ROTTEN_MEAT_X_2 = "Rotten Meat x 2"
|
||||
ROTTEN_MEAT_X_8 = "Rotten Meat x 8"
|
||||
SEA_LOAF_X_2 = "Sea Loaf x 2"
|
||||
SMALL_BONE_X_3 = "Small Bone x 3"
|
||||
SMALL_EGG_X_2 = "Small Egg x 2"
|
||||
LI_AND_LI_SONG = "Li and Li Song"
|
||||
SHIELD_SONG = "Shield Song"
|
||||
BEAST_FORM = "Beast Form"
|
||||
SUN_FORM = "Sun Form"
|
||||
NATURE_FORM = "Nature Form"
|
||||
ENERGY_FORM = "Energy Form"
|
||||
BIND_SONG = "Bind Song"
|
||||
FISH_FORM = "Fish Form"
|
||||
SPIRIT_FORM = "Spirit Form"
|
||||
DUAL_FORM = "Dual Form"
|
||||
TRANSTURTLE_VEIL_TOP_LEFT = "Transturtle Veil top left"
|
||||
TRANSTURTLE_VEIL_TOP_RIGHT = "Transturtle Veil top right"
|
||||
TRANSTURTLE_OPEN_WATERS = "Transturtle Open Waters top right"
|
||||
TRANSTURTLE_KELP_FOREST = "Transturtle Kelp Forest bottom left"
|
||||
TRANSTURTLE_HOME_WATERS = "Transturtle Home Waters"
|
||||
TRANSTURTLE_ABYSS = "Transturtle Abyss right"
|
||||
TRANSTURTLE_BODY = "Transturtle Final Boss"
|
||||
TRANSTURTLE_SIMON_SAYS = "Transturtle Simon Says"
|
||||
TRANSTURTLE_ARNASSI_RUINS = "Transturtle Arnassi Ruins"
|
||||
# Events name
|
||||
BODY_TONGUE_CLEARED = "Body Tongue cleared"
|
||||
HAS_SUN_CRYSTAL = "Has Sun Crystal"
|
||||
FALLEN_GOD_BEATED = "Fallen God beated"
|
||||
MITHALAN_GOD_BEATED = "Mithalan God beated"
|
||||
DRUNIAN_GOD_BEATED = "Drunian God beated"
|
||||
LUMEREAN_GOD_BEATED = "Lumerean God beated"
|
||||
THE_GOLEM_BEATED = "The Golem beated"
|
||||
NAUTILUS_PRIME_BEATED = "Nautilus Prime beated"
|
||||
BLASTER_PEG_PRIME_BEATED = "Blaster Peg Prime beated"
|
||||
MERGOG_BEATED = "Mergog beated"
|
||||
MITHALAN_PRIESTS_BEATED = "Mithalan priests beated"
|
||||
OCTOPUS_PRIME_BEATED = "Octopus Prime beated"
|
||||
CRABBIUS_MAXIMUS_BEATED = "Crabbius Maximus beated"
|
||||
MANTIS_SHRIMP_PRIME_BEATED = "Mantis Shrimp Prime beated"
|
||||
KING_JELLYFISH_GOD_PRIME_BEATED = "King Jellyfish God Prime beated"
|
||||
VICTORY = "Victory"
|
||||
FIRST_SECRET_OBTAINED = "First Secret obtained"
|
||||
SECOND_SECRET_OBTAINED = "Second Secret obtained"
|
||||
THIRD_SECRET_OBTAINED = "Third Secret obtained"
|
||||
|
||||
"""Information data for every (not event) item."""
|
||||
item_table = {
|
||||
# name: ID, Nb, Item Type, Item Group
|
||||
"Anemone": ItemData(698000, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_anemone
|
||||
"Arnassi Statue": ItemData(698001, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_arnassi_statue
|
||||
"Big Seed": ItemData(698002, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_big_seed
|
||||
"Glowing Seed": ItemData(698003, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_bio_seed
|
||||
"Black Pearl": ItemData(698004, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_blackpearl
|
||||
"Baby Blaster": ItemData(698005, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_blaster
|
||||
"Crab Armor": ItemData(698006, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_crab_costume
|
||||
"Baby Dumbo": ItemData(698007, 1, ItemType.PROGRESSION, ItemGroup.UTILITY), # collectible_dumbo
|
||||
"Tooth": ItemData(698008, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_energy_boss
|
||||
"Energy Statue": ItemData(698009, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_energy_statue
|
||||
"Krotite Armor": ItemData(698010, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_energy_temple
|
||||
"Golden Starfish": ItemData(698011, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_gold_star
|
||||
"Golden Gear": ItemData(698012, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_golden_gear
|
||||
"Jelly Beacon": ItemData(698013, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_jelly_beacon
|
||||
"Jelly Costume": ItemData(698014, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_jelly_costume
|
||||
"Jelly Plant": ItemData(698015, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_jelly_plant
|
||||
"Mithalas Doll": ItemData(698016, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithala_doll
|
||||
"Mithalan Dress": ItemData(698017, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithalan_costume
|
||||
"Mithalas Banner": ItemData(698018, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithalas_banner
|
||||
"Mithalas Pot": ItemData(698019, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithalas_pot
|
||||
"Mutant Costume": ItemData(698020, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mutant_costume
|
||||
"Baby Nautilus": ItemData(698021, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_nautilus
|
||||
"Baby Piranha": ItemData(698022, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_piranha
|
||||
"Arnassi Armor": ItemData(698023, 1, ItemType.PROGRESSION, ItemGroup.UTILITY), # collectible_seahorse_costume
|
||||
"Seed Bag": ItemData(698024, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_seed_bag
|
||||
"King's Skull": ItemData(698025, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_skull
|
||||
"Song Plant Spore": ItemData(698026, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_spore_seed
|
||||
"Stone Head": ItemData(698027, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_stone_head
|
||||
"Sun Key": ItemData(698028, 1, ItemType.NORMAL, ItemGroup.COLLECTIBLE), # collectible_sun_key
|
||||
"Girl Costume": ItemData(698029, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_teen_costume
|
||||
"Odd Container": ItemData(698030, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_treasure_chest
|
||||
"Trident": ItemData(698031, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_trident_head
|
||||
"Turtle Egg": ItemData(698032, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_turtle_egg
|
||||
"Jelly Egg": ItemData(698033, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_upsidedown_seed
|
||||
"Urchin Costume": ItemData(698034, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_urchin_costume
|
||||
"Baby Walker": ItemData(698035, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_walker
|
||||
"Vedha's Cure-All-All": ItemData(698036, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_Vedha'sCure-All
|
||||
"Zuuna's perogi": ItemData(698037, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_Zuuna'sperogi
|
||||
"Arcane poultice": ItemData(698038, 7, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_arcanepoultice
|
||||
"Berry ice cream": ItemData(698039, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_berryicecream
|
||||
"Buttery sea loaf": ItemData(698040, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_butterysealoaf
|
||||
"Cold borscht": ItemData(698041, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_coldborscht
|
||||
"Cold soup": ItemData(698042, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_coldsoup
|
||||
"Crab cake": ItemData(698043, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_crabcake
|
||||
"Divine soup": ItemData(698044, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_divinesoup
|
||||
"Dumbo ice cream": ItemData(698045, 3, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_dumboicecream
|
||||
"Fish oil": ItemData(698046, 2, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_fishoil
|
||||
"Glowing egg": ItemData(698047, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_glowingegg
|
||||
"Hand roll": ItemData(698048, 5, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_handroll
|
||||
"Healing poultice": ItemData(698049, 4, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_healingpoultice
|
||||
"Hearty soup": ItemData(698050, 5, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_heartysoup
|
||||
"Hot borscht": ItemData(698051, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_hotborscht
|
||||
"Hot soup": ItemData(698052, 3, ItemType.PROGRESSION, ItemGroup.RECIPE), # ingredient_hotsoup
|
||||
"Ice cream": ItemData(698053, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_icecream
|
||||
"Leadership roll": ItemData(698054, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_leadershiproll
|
||||
"Leaf poultice": ItemData(698055, 5, ItemType.PROGRESSION, ItemGroup.RECIPE), # ingredient_leafpoultice
|
||||
"Leeching poultice": ItemData(698056, 4, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_leechingpoultice
|
||||
"Legendary cake": ItemData(698057, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_legendarycake
|
||||
"Loaf of life": ItemData(698058, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_loafoflife
|
||||
"Long life soup": ItemData(698059, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_longlifesoup
|
||||
"Magic soup": ItemData(698060, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_magicsoup
|
||||
"Mushroom x 2": ItemData(698061, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_mushroom
|
||||
"Perogi": ItemData(698062, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_perogi
|
||||
"Plant leaf": ItemData(698063, 3, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_plantleaf
|
||||
"Plump perogi": ItemData(698064, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_plumpperogi
|
||||
"Poison loaf": ItemData(698065, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_poisonloaf
|
||||
"Poison soup": ItemData(698066, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_poisonsoup
|
||||
"Rainbow mushroom": ItemData(698067, 4, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_rainbowmushroom
|
||||
"Rainbow soup": ItemData(698068, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_rainbowsoup
|
||||
"Red berry": ItemData(698069, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_redberry
|
||||
"Red bulb x 2": ItemData(698070, 3, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_redbulb
|
||||
"Rotten cake": ItemData(698071, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_rottencake
|
||||
"Rotten loaf x 8": ItemData(698072, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_rottenloaf
|
||||
"Rotten meat": ItemData(698073, 5, ItemType.JUNK, ItemGroup.INGREDIENT), # ingredient_rottenmeat
|
||||
"Royal soup": ItemData(698074, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_royalsoup
|
||||
"Sea cake": ItemData(698075, 4, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_seacake
|
||||
"Sea loaf": ItemData(698076, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_sealoaf
|
||||
"Shark fin soup": ItemData(698077, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_sharkfinsoup
|
||||
"Sight poultice": ItemData(698078, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_sightpoultice
|
||||
"Small bone x 2": ItemData(698079, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_smallbone
|
||||
"Small egg": ItemData(698080, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_smallegg
|
||||
"Small tentacle x 2": ItemData(698081, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_smalltentacle
|
||||
"Special bulb": ItemData(698082, 5, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_specialbulb
|
||||
"Special cake": ItemData(698083, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_specialcake
|
||||
"Spicy meat x 2": ItemData(698084, 3, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_spicymeat
|
||||
"Spicy roll": ItemData(698085, 11, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_spicyroll
|
||||
"Spicy soup": ItemData(698086, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_spicysoup
|
||||
"Spider roll": ItemData(698087, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_spiderroll
|
||||
"Swamp cake": ItemData(698088, 3, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_swampcake
|
||||
"Tasty cake": ItemData(698089, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_tastycake
|
||||
"Tasty roll": ItemData(698090, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_tastyroll
|
||||
"Tough cake": ItemData(698091, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_toughcake
|
||||
"Turtle soup": ItemData(698092, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_turtlesoup
|
||||
"Vedha sea crisp": ItemData(698093, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_vedhaseacrisp
|
||||
"Veggie cake": ItemData(698094, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_veggiecake
|
||||
"Veggie ice cream": ItemData(698095, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_veggieicecream
|
||||
"Veggie soup": ItemData(698096, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_veggiesoup
|
||||
"Volcano roll": ItemData(698097, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_volcanoroll
|
||||
"Health upgrade": ItemData(698098, 5, ItemType.NORMAL, ItemGroup.HEALTH), # upgrade_health_?
|
||||
"Wok": ItemData(698099, 1, ItemType.NORMAL, ItemGroup.UTILITY), # upgrade_wok
|
||||
"Eel oil x 2": ItemData(698100, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_eeloil
|
||||
"Fish meat x 2": ItemData(698101, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_fishmeat
|
||||
"Fish oil x 3": ItemData(698102, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_fishoil
|
||||
"Glowing egg x 2": ItemData(698103, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_glowingegg
|
||||
"Healing poultice x 2": ItemData(698104, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_healingpoultice
|
||||
"Hot soup x 2": ItemData(698105, 1, ItemType.PROGRESSION, ItemGroup.RECIPE), # ingredient_hotsoup
|
||||
"Leadership roll x 2": ItemData(698106, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_leadershiproll
|
||||
"Leaf poultice x 3": ItemData(698107, 2, ItemType.PROGRESSION, ItemGroup.RECIPE), # ingredient_leafpoultice
|
||||
"Plant leaf x 2": ItemData(698108, 2, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_plantleaf
|
||||
"Plant leaf x 3": ItemData(698109, 4, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_plantleaf
|
||||
"Rotten meat x 2": ItemData(698110, 1, ItemType.JUNK, ItemGroup.INGREDIENT), # ingredient_rottenmeat
|
||||
"Rotten meat x 8": ItemData(698111, 1, ItemType.JUNK, ItemGroup.INGREDIENT), # ingredient_rottenmeat
|
||||
"Sea loaf x 2": ItemData(698112, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_sealoaf
|
||||
"Small bone x 3": ItemData(698113, 3, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_smallbone
|
||||
"Small egg x 2": ItemData(698114, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_smallegg
|
||||
"Li and Li song": ItemData(698115, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_li
|
||||
"Shield song": ItemData(698116, 1, ItemType.NORMAL, ItemGroup.SONG), # song_shield
|
||||
"Beast form": ItemData(698117, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_beast
|
||||
"Sun form": ItemData(698118, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_sun
|
||||
"Nature form": ItemData(698119, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_nature
|
||||
"Energy form": ItemData(698120, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_energy
|
||||
"Bind song": ItemData(698121, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_bind
|
||||
"Fish form": ItemData(698122, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_fish
|
||||
"Spirit form": ItemData(698123, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_spirit
|
||||
"Dual form": ItemData(698124, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_dual
|
||||
"Transturtle Veil top left": ItemData(698125, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_veil01
|
||||
"Transturtle Veil top right": ItemData(698126, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_veil02
|
||||
"Transturtle Open Water top right": ItemData(698127, 1, ItemType.PROGRESSION,
|
||||
ItemGroup.TURTLE), # transport_openwater03
|
||||
"Transturtle Forest bottom left": ItemData(698128, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_forest04
|
||||
"Transturtle Home Water": ItemData(698129, 1, ItemType.NORMAL, ItemGroup.TURTLE), # transport_mainarea
|
||||
"Transturtle Abyss right": ItemData(698130, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_abyss03
|
||||
"Transturtle Final Boss": ItemData(698131, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_finalboss
|
||||
"Transturtle Simon Says": ItemData(698132, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_forest05
|
||||
"Transturtle Arnassi Ruins": ItemData(698133, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_seahorse
|
||||
ItemNames.ANEMONE: ItemData(698000, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_anemone
|
||||
ItemNames.ARNASSI_STATUE: ItemData(698001, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_arnassi_statue
|
||||
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.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
|
||||
ItemNames.ENERGY_STATUE: ItemData(698009, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_energy_statue
|
||||
ItemNames.KROTITE_ARMOR: ItemData(698010, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_energy_temple
|
||||
ItemNames.GOLDEN_STARFISH: ItemData(698011, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_gold_star
|
||||
ItemNames.GOLDEN_GEAR: ItemData(698012, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_golden_gear
|
||||
ItemNames.JELLY_BEACON: ItemData(698013, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_jelly_beacon
|
||||
ItemNames.JELLY_COSTUME: ItemData(698014, 1, ItemType.NORMAL, ItemGroup.UTILITY), # collectible_jelly_costume
|
||||
ItemNames.JELLY_PLANT: ItemData(698015, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_jelly_plant
|
||||
ItemNames.MITHALAS_DOLL: ItemData(698016, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithala_doll
|
||||
ItemNames.MITHALAN_DRESS: ItemData(698017, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_mithalan_costume
|
||||
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.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
|
||||
ItemNames.SONG_PLANT_SPORE: ItemData(698026, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_spore_seed
|
||||
ItemNames.STONE_HEAD: ItemData(698027, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_stone_head
|
||||
ItemNames.SUN_KEY: ItemData(698028, 1, ItemType.NORMAL, ItemGroup.COLLECTIBLE), # collectible_sun_key
|
||||
ItemNames.GIRL_COSTUME: ItemData(698029, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_teen_costume
|
||||
ItemNames.ODD_CONTAINER: ItemData(698030, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_treasure_chest
|
||||
ItemNames.TRIDENT: ItemData(698031, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_trident_head
|
||||
ItemNames.TURTLE_EGG: ItemData(698032, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_turtle_egg
|
||||
ItemNames.JELLY_EGG: ItemData(698033, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_upsidedown_seed
|
||||
ItemNames.URCHIN_COSTUME: ItemData(698034, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_urchin_costume
|
||||
ItemNames.BABY_WALKER: ItemData(698035, 1, ItemType.JUNK, ItemGroup.COLLECTIBLE), # collectible_walker
|
||||
ItemNames.VEDHA_S_CURE_ALL: ItemData(698036, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_Vedha'sCure-All
|
||||
ItemNames.ZUUNA_S_PEROGI: ItemData(698037, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_Zuuna'sperogi
|
||||
ItemNames.ARCANE_POULTICE: ItemData(698038, 7, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_arcanepoultice
|
||||
ItemNames.BERRY_ICE_CREAM: ItemData(698039, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_berryicecream
|
||||
ItemNames.BUTTERY_SEA_LOAF: ItemData(698040, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_butterysealoaf
|
||||
ItemNames.COLD_BORSCHT: ItemData(698041, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_coldborscht
|
||||
ItemNames.COLD_SOUP: ItemData(698042, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_coldsoup
|
||||
ItemNames.CRAB_CAKE: ItemData(698043, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_crabcake
|
||||
ItemNames.DIVINE_SOUP: ItemData(698044, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_divinesoup
|
||||
ItemNames.DUMBO_ICE_CREAM: ItemData(698045, 3, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_dumboicecream
|
||||
ItemNames.FISH_OIL: ItemData(698046, 2, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_fishoil
|
||||
ItemNames.GLOWING_EGG: ItemData(698047, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_glowingegg
|
||||
ItemNames.HAND_ROLL: ItemData(698048, 5, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_handroll
|
||||
ItemNames.HEALING_POULTICE: ItemData(698049, 4, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_healingpoultice
|
||||
ItemNames.HEARTY_SOUP: ItemData(698050, 5, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_heartysoup
|
||||
ItemNames.HOT_BORSCHT: ItemData(698051, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_hotborscht
|
||||
ItemNames.HOT_SOUP: ItemData(698052, 3, ItemType.PROGRESSION, ItemGroup.RECIPE), # ingredient_hotsoup
|
||||
ItemNames.ICE_CREAM: ItemData(698053, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_icecream
|
||||
ItemNames.LEADERSHIP_ROLL: ItemData(698054, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_leadershiproll
|
||||
ItemNames.LEAF_POULTICE: ItemData(698055, 5, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_leafpoultice
|
||||
ItemNames.LEECHING_POULTICE: ItemData(698056, 4, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_leechingpoultice
|
||||
ItemNames.LEGENDARY_CAKE: ItemData(698057, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_legendarycake
|
||||
ItemNames.LOAF_OF_LIFE: ItemData(698058, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_loafoflife
|
||||
ItemNames.LONG_LIFE_SOUP: ItemData(698059, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_longlifesoup
|
||||
ItemNames.MAGIC_SOUP: ItemData(698060, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_magicsoup
|
||||
ItemNames.MUSHROOM_X_2: ItemData(698061, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_mushroom
|
||||
ItemNames.PEROGI: ItemData(698062, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_perogi
|
||||
ItemNames.PLANT_LEAF: ItemData(698063, 3, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_plantleaf
|
||||
ItemNames.PLUMP_PEROGI: ItemData(698064, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_plumpperogi
|
||||
ItemNames.POISON_LOAF: ItemData(698065, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_poisonloaf
|
||||
ItemNames.POISON_SOUP: ItemData(698066, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_poisonsoup
|
||||
ItemNames.RAINBOW_MUSHROOM: ItemData(698067, 4, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_rainbowmushroom
|
||||
ItemNames.RAINBOW_SOUP: ItemData(698068, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_rainbowsoup
|
||||
ItemNames.RED_BERRY: ItemData(698069, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_redberry
|
||||
ItemNames.RED_BULB_X_2: ItemData(698070, 3, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_redbulb
|
||||
ItemNames.ROTTEN_CAKE: ItemData(698071, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_rottencake
|
||||
ItemNames.ROTTEN_LOAF_X_8: ItemData(698072, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_rottenloaf
|
||||
ItemNames.ROTTEN_MEAT: ItemData(698073, 5, ItemType.JUNK, ItemGroup.INGREDIENT), # ingredient_rottenmeat
|
||||
ItemNames.ROYAL_SOUP: ItemData(698074, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_royalsoup
|
||||
ItemNames.SEA_CAKE: ItemData(698075, 4, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_seacake
|
||||
ItemNames.SEA_LOAF: ItemData(698076, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_sealoaf
|
||||
ItemNames.SHARK_FIN_SOUP: ItemData(698077, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_sharkfinsoup
|
||||
ItemNames.SIGHT_POULTICE: ItemData(698078, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_sightpoultice
|
||||
ItemNames.SMALL_BONE_X_2: ItemData(698079, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_smallbone
|
||||
ItemNames.SMALL_EGG: ItemData(698080, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_smallegg
|
||||
ItemNames.SMALL_TENTACLE_X_2: ItemData(698081, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_smalltentacle
|
||||
ItemNames.SPECIAL_BULB: ItemData(698082, 5, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_specialbulb
|
||||
ItemNames.SPECIAL_CAKE: ItemData(698083, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_specialcake
|
||||
ItemNames.SPICY_MEAT_X_2: ItemData(698084, 3, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_spicymeat
|
||||
ItemNames.SPICY_ROLL: ItemData(698085, 11, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_spicyroll
|
||||
ItemNames.SPICY_SOUP: ItemData(698086, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_spicysoup
|
||||
ItemNames.SPIDER_ROLL: ItemData(698087, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_spiderroll
|
||||
ItemNames.SWAMP_CAKE: ItemData(698088, 3, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_swampcake
|
||||
ItemNames.TASTY_CAKE: ItemData(698089, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_tastycake
|
||||
ItemNames.TASTY_ROLL: ItemData(698090, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_tastyroll
|
||||
ItemNames.TOUGH_CAKE: ItemData(698091, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_toughcake
|
||||
ItemNames.TURTLE_SOUP: ItemData(698092, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_turtlesoup
|
||||
ItemNames.VEDHA_SEA_CRISP: ItemData(698093, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_vedhaseacrisp
|
||||
ItemNames.VEGGIE_CAKE: ItemData(698094, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_veggiecake
|
||||
ItemNames.VEGGIE_ICE_CREAM: ItemData(698095, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_veggieicecream
|
||||
ItemNames.VEGGIE_SOUP: ItemData(698096, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_veggiesoup
|
||||
ItemNames.VOLCANO_ROLL: ItemData(698097, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_volcanoroll
|
||||
ItemNames.HEALTH_UPGRADE: ItemData(698098, 5, ItemType.NORMAL, ItemGroup.HEALTH), # upgrade_health_?
|
||||
ItemNames.WOK: ItemData(698099, 1, ItemType.NORMAL, ItemGroup.UTILITY), # upgrade_wok
|
||||
ItemNames.EEL_OIL_X_2: ItemData(698100, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_eeloil
|
||||
ItemNames.FISH_MEAT_X_2: ItemData(698101, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_fishmeat
|
||||
ItemNames.FISH_OIL_X_3: ItemData(698102, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_fishoil
|
||||
ItemNames.GLOWING_EGG_X_2: ItemData(698103, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_glowingegg
|
||||
ItemNames.HEALING_POULTICE_X_2: ItemData(698104, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_healingpoultice
|
||||
ItemNames.HOT_SOUP_X_2: ItemData(698105, 1, ItemType.PROGRESSION, ItemGroup.RECIPE), # ingredient_hotsoup
|
||||
ItemNames.LEADERSHIP_ROLL_X_2: ItemData(698106, 1, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_leadershiproll
|
||||
ItemNames.LEAF_POULTICE_X_3: ItemData(698107, 2, ItemType.NORMAL, ItemGroup.RECIPE), # ingredient_leafpoultice
|
||||
ItemNames.PLANT_LEAF_X_2: ItemData(698108, 2, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_plantleaf
|
||||
ItemNames.PLANT_LEAF_X_3: ItemData(698109, 4, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_plantleaf
|
||||
ItemNames.ROTTEN_MEAT_X_2: ItemData(698110, 1, ItemType.JUNK, ItemGroup.INGREDIENT), # ingredient_rottenmeat
|
||||
ItemNames.ROTTEN_MEAT_X_8: ItemData(698111, 1, ItemType.JUNK, ItemGroup.INGREDIENT), # ingredient_rottenmeat
|
||||
ItemNames.SEA_LOAF_X_2: ItemData(698112, 1, ItemType.JUNK, ItemGroup.RECIPE), # ingredient_sealoaf
|
||||
ItemNames.SMALL_BONE_X_3: ItemData(698113, 3, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_smallbone
|
||||
ItemNames.SMALL_EGG_X_2: ItemData(698114, 1, ItemType.NORMAL, ItemGroup.INGREDIENT), # ingredient_smallegg
|
||||
ItemNames.LI_AND_LI_SONG: ItemData(698115, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_li
|
||||
ItemNames.SHIELD_SONG: ItemData(698116, 1, ItemType.NORMAL, ItemGroup.SONG), # song_shield
|
||||
ItemNames.BEAST_FORM: ItemData(698117, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_beast
|
||||
ItemNames.SUN_FORM: ItemData(698118, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_sun
|
||||
ItemNames.NATURE_FORM: ItemData(698119, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_nature
|
||||
ItemNames.ENERGY_FORM: ItemData(698120, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_energy
|
||||
ItemNames.BIND_SONG: ItemData(698121, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_bind
|
||||
ItemNames.FISH_FORM: ItemData(698122, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_fish
|
||||
ItemNames.SPIRIT_FORM: ItemData(698123, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_spirit
|
||||
ItemNames.DUAL_FORM: ItemData(698124, 1, ItemType.PROGRESSION, ItemGroup.SONG), # song_dual
|
||||
ItemNames.TRANSTURTLE_VEIL_TOP_LEFT: ItemData(698125, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_veil01
|
||||
ItemNames.TRANSTURTLE_VEIL_TOP_RIGHT: ItemData(698126, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_veil02
|
||||
ItemNames.TRANSTURTLE_OPEN_WATERS: ItemData(698127, 1, ItemType.PROGRESSION,
|
||||
ItemGroup.TURTLE), # transport_openwater03
|
||||
ItemNames.TRANSTURTLE_KELP_FOREST: ItemData(698128, 1, ItemType.PROGRESSION, ItemGroup.TURTLE),
|
||||
# transport_forest04
|
||||
ItemNames.TRANSTURTLE_HOME_WATERS: ItemData(698129, 1, ItemType.NORMAL, ItemGroup.TURTLE), # transport_mainarea
|
||||
ItemNames.TRANSTURTLE_ABYSS: ItemData(698130, 1, ItemType.PROGRESSION, ItemGroup.TURTLE), # transport_abyss03
|
||||
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
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -15,7 +15,10 @@ class IngredientRandomizer(Choice):
|
||||
"""
|
||||
display_name = "Randomize Ingredients"
|
||||
option_off = 0
|
||||
alias_false = 0
|
||||
option_common_ingredients = 1
|
||||
alias_on = 1
|
||||
alias_true = 1
|
||||
option_all_ingredients = 2
|
||||
default = 0
|
||||
|
||||
@@ -29,14 +32,43 @@ class TurtleRandomizer(Choice):
|
||||
"""Randomize the transportation turtle."""
|
||||
display_name = "Turtle Randomizer"
|
||||
option_none = 0
|
||||
alias_off = 0
|
||||
alias_false = 0
|
||||
option_all = 1
|
||||
option_all_except_final = 2
|
||||
alias_on = 2
|
||||
alias_true = 2
|
||||
default = 2
|
||||
|
||||
|
||||
class EarlyEnergyForm(DefaultOnToggle):
|
||||
""" Force the Energy Form to be in a location early in the game """
|
||||
display_name = "Early Energy Form"
|
||||
class EarlyBindSong(Choice):
|
||||
"""
|
||||
Force the Bind song to be in a location early in the multiworld (or directly in your world if Early and Local is
|
||||
selected).
|
||||
"""
|
||||
display_name = "Early Bind song"
|
||||
option_off = 0
|
||||
alias_false = 0
|
||||
option_early = 1
|
||||
alias_on = 1
|
||||
alias_true = 1
|
||||
option_early_and_local = 2
|
||||
default = 1
|
||||
|
||||
|
||||
class EarlyEnergyForm(Choice):
|
||||
"""
|
||||
Force the Energy form to be in a location early in the multiworld (or directly in your world if Early and Local is
|
||||
selected).
|
||||
"""
|
||||
display_name = "Early Energy form"
|
||||
option_off = 0
|
||||
alias_false = 0
|
||||
option_early = 1
|
||||
alias_on = 1
|
||||
alias_true = 1
|
||||
option_early_and_local = 2
|
||||
default = 1
|
||||
|
||||
|
||||
class AquarianTranslation(Toggle):
|
||||
@@ -47,7 +79,7 @@ class AquarianTranslation(Toggle):
|
||||
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", "Sun God" and "The Golem".
|
||||
"Fallen God", "Mithalan God", "Drunian God", "Lumerean God" and "The Golem".
|
||||
"""
|
||||
display_name = "Big bosses to beat"
|
||||
range_start = 0
|
||||
@@ -104,7 +136,7 @@ class LightNeededToGetToDarkPlaces(DefaultOnToggle):
|
||||
display_name = "Light needed to get to dark places"
|
||||
|
||||
|
||||
class BindSongNeededToGetUnderRockBulb(Toggle):
|
||||
class BindSongNeededToGetUnderRockBulb(DefaultOnToggle):
|
||||
"""
|
||||
Make sure that the bind song can be acquired before having to obtain sing bulbs under rocks.
|
||||
"""
|
||||
@@ -121,13 +153,18 @@ class BlindGoal(Toggle):
|
||||
|
||||
class UnconfineHomeWater(Choice):
|
||||
"""
|
||||
Open the way out of the Home Water area so that Naija can go to open water and beyond without the bind song.
|
||||
Open the way out of the Home Waters area so that Naija can go to open water and beyond without the bind song.
|
||||
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 Water Area"
|
||||
display_name = "Unconfine Home Waters Area"
|
||||
option_off = 0
|
||||
alias_false = 0
|
||||
option_via_energy_door = 1
|
||||
option_via_transturtle = 2
|
||||
option_via_both = 3
|
||||
alias_on = 3
|
||||
alias_true = 3
|
||||
default = 0
|
||||
|
||||
|
||||
@@ -142,6 +179,7 @@ class AquariaOptions(PerGameCommonOptions):
|
||||
big_bosses_to_beat: BigBossesToBeat
|
||||
turtle_randomizer: TurtleRandomizer
|
||||
early_energy_form: EarlyEnergyForm
|
||||
early_bind_song: EarlyBindSong
|
||||
light_needed_to_get_to_dark_places: LightNeededToGetToDarkPlaces
|
||||
bind_song_needed_to_get_under_rock_bulb: BindSongNeededToGetUnderRockBulb
|
||||
unconfine_home_water: UnconfineHomeWater
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -7,9 +7,10 @@ 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
|
||||
from .Locations import location_table
|
||||
from .Options import AquariaOptions
|
||||
from .Items import item_table, AquariaItem, ItemType, ItemGroup, ItemNames
|
||||
from .Locations import location_table, AquariaLocationNames
|
||||
from .Options import (AquariaOptions, IngredientRandomizer, TurtleRandomizer, EarlyBindSong, EarlyEnergyForm,
|
||||
UnconfineHomeWater, Objective)
|
||||
from .Regions import AquariaRegions
|
||||
|
||||
|
||||
@@ -65,15 +66,15 @@ class AquariaWorld(World):
|
||||
web: WebWorld = AquariaWeb()
|
||||
"The web page generation informations"
|
||||
|
||||
item_name_to_id: ClassVar[Dict[str, int]] =\
|
||||
item_name_to_id: ClassVar[Dict[str, int]] = \
|
||||
{name: data.id for name, data in item_table.items()}
|
||||
"The name and associated ID of each item of the world"
|
||||
|
||||
item_name_groups = {
|
||||
"Damage": {"Energy form", "Nature form", "Beast form",
|
||||
"Li and Li song", "Baby Nautilus", "Baby Piranha",
|
||||
"Baby Blaster"},
|
||||
"Light": {"Sun form", "Baby Dumbo"}
|
||||
"Damage": {ItemNames.ENERGY_FORM, ItemNames.NATURE_FORM, ItemNames.BEAST_FORM,
|
||||
ItemNames.LI_AND_LI_SONG, ItemNames.BABY_NAUTILUS, ItemNames.BABY_PIRANHA,
|
||||
ItemNames.BABY_BLASTER},
|
||||
"Light": {ItemNames.SUN_FORM, ItemNames.BABY_DUMBO}
|
||||
}
|
||||
"""Grouping item make it easier to find them"""
|
||||
|
||||
@@ -148,23 +149,32 @@ 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.turtle_randomizer.value > 0:
|
||||
if self.options.turtle_randomizer.value == 2:
|
||||
self.__pre_fill_item("Transturtle Final Boss", "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:
|
||||
self.__pre_fill_item(ItemNames.TRANSTURTLE_BODY, AquariaLocationNames.FINAL_BOSS_AREA_TRANSTURTLE,
|
||||
precollected)
|
||||
else:
|
||||
self.__pre_fill_item("Transturtle Veil top left", "The Veil top left area, Transturtle", precollected)
|
||||
self.__pre_fill_item("Transturtle Veil top right", "The Veil top right area, Transturtle", precollected)
|
||||
self.__pre_fill_item("Transturtle Open Water top right", "Open Water top right area, Transturtle",
|
||||
self.__pre_fill_item(ItemNames.TRANSTURTLE_VEIL_TOP_LEFT,
|
||||
AquariaLocationNames.THE_VEIL_TOP_LEFT_AREA_TRANSTURTLE, precollected)
|
||||
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)
|
||||
self.__pre_fill_item("Transturtle Forest bottom left", "Kelp Forest bottom left area, Transturtle",
|
||||
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)
|
||||
self.__pre_fill_item("Transturtle Home Water", "Home Water, Transturtle", precollected)
|
||||
self.__pre_fill_item("Transturtle Abyss right", "Abyss right area, Transturtle", precollected)
|
||||
self.__pre_fill_item("Transturtle Final Boss", "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("Transturtle Simon Says", "Arnassi Ruins, Transturtle", precollected,
|
||||
ItemClassification.progression)
|
||||
self.__pre_fill_item("Transturtle Arnassi Ruins", "Simon Says area, Transturtle", precollected)
|
||||
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)
|
||||
for name, data in item_table.items():
|
||||
if name not in self.exclude:
|
||||
for i in range(data.count):
|
||||
@@ -175,10 +185,17 @@ class AquariaWorld(World):
|
||||
"""
|
||||
Launched when the Multiworld generator is ready to generate rules
|
||||
"""
|
||||
|
||||
if self.options.early_energy_form == EarlyEnergyForm.option_early:
|
||||
self.multiworld.early_items[self.player][ItemNames.ENERGY_FORM] = 1
|
||||
elif self.options.early_energy_form == EarlyEnergyForm.option_early_and_local:
|
||||
self.multiworld.local_early_items[self.player][ItemNames.ENERGY_FORM] = 1
|
||||
if self.options.early_bind_song == EarlyBindSong.option_early:
|
||||
self.multiworld.early_items[self.player][ItemNames.BIND_SONG] = 1
|
||||
elif self.options.early_bind_song == EarlyBindSong.option_early_and_local:
|
||||
self.multiworld.local_early_items[self.player][ItemNames.BIND_SONG] = 1
|
||||
self.regions.adjusting_rules(self.options)
|
||||
self.multiworld.completion_condition[self.player] = lambda \
|
||||
state: state.has("Victory", self.player)
|
||||
state: state.has(ItemNames.VICTORY, self.player)
|
||||
|
||||
def generate_basic(self) -> None:
|
||||
"""
|
||||
@@ -186,13 +203,13 @@ class AquariaWorld(World):
|
||||
Used to fill then `ingredients_substitution` list
|
||||
"""
|
||||
simple_ingredients_substitution = [i for i in range(27)]
|
||||
if self.options.ingredient_randomizer.value > 0:
|
||||
if self.options.ingredient_randomizer.value == 1:
|
||||
if self.options.ingredient_randomizer.value > IngredientRandomizer.option_off:
|
||||
if self.options.ingredient_randomizer.value == IngredientRandomizer.option_common_ingredients:
|
||||
simple_ingredients_substitution.pop(-1)
|
||||
simple_ingredients_substitution.pop(-1)
|
||||
simple_ingredients_substitution.pop(-1)
|
||||
self.random.shuffle(simple_ingredients_substitution)
|
||||
if self.options.ingredient_randomizer.value == 1:
|
||||
if self.options.ingredient_randomizer.value == IngredientRandomizer.option_common_ingredients:
|
||||
simple_ingredients_substitution.extend([24, 25, 26])
|
||||
dishes_substitution = [i for i in range(27, 76)]
|
||||
if self.options.dish_randomizer:
|
||||
@@ -205,14 +222,19 @@ class AquariaWorld(World):
|
||||
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 > 0,
|
||||
"secret_needed":
|
||||
self.options.objective.value == Objective.option_obtain_secrets_and_kill_the_creator,
|
||||
"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),
|
||||
"unconfine_home_water_energy_door": self.options.unconfine_home_water.value in [1, 3],
|
||||
"unconfine_home_water_transturtle": self.options.unconfine_home_water.value in [2, 3],
|
||||
"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,
|
||||
"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
|
||||
}
|
||||
|
||||
@@ -24,7 +24,7 @@ The locations in the randomizer are:
|
||||
* Beating Mithalan God boss
|
||||
* Fish Cave puzzle
|
||||
* Beating Drunian God boss
|
||||
* Beating Sun God boss
|
||||
* Beating Lumerean God boss
|
||||
* Breaking Li cage in the body
|
||||
|
||||
Note that, unlike the vanilla game, when opening sing bulbs, Mithalas urns and Sunken City crates,
|
||||
|
||||
@@ -6,211 +6,212 @@ Description: Base class for the Aquaria randomizer unit tests
|
||||
|
||||
|
||||
from test.bases import WorldTestBase
|
||||
from ..Locations import AquariaLocationNames
|
||||
|
||||
# Every location accessible after the home water.
|
||||
after_home_water_locations = [
|
||||
"Sun Crystal",
|
||||
"Home Water, Transturtle",
|
||||
"Open Water top left area, bulb under the rock in the right path",
|
||||
"Open Water top left area, bulb under the rock in the left path",
|
||||
"Open Water top left area, bulb to the right of the save crystal",
|
||||
"Open Water top right area, bulb in the small path before Mithalas",
|
||||
"Open Water top right area, bulb in the path from the left entrance",
|
||||
"Open Water top right area, bulb in the clearing close to the bottom exit",
|
||||
"Open Water top right area, bulb in the big clearing close to the save crystal",
|
||||
"Open Water top right area, bulb in the big clearing to the top exit",
|
||||
"Open Water top right area, first urn in the Mithalas exit",
|
||||
"Open Water top right area, second urn in the Mithalas exit",
|
||||
"Open Water top right area, third urn in the Mithalas exit",
|
||||
"Open Water top right area, bulb in the turtle room",
|
||||
"Open Water top right area, Transturtle",
|
||||
"Open Water bottom left area, bulb behind the chomper fish",
|
||||
"Open Water bottom left area, bulb inside the lowest fish pass",
|
||||
"Open Water skeleton path, bulb close to the right exit",
|
||||
"Open Water skeleton path, bulb behind the chomper fish",
|
||||
"Open Water skeleton path, King Skull",
|
||||
"Arnassi Ruins, bulb in the right part",
|
||||
"Arnassi Ruins, bulb in the left part",
|
||||
"Arnassi Ruins, bulb in the center part",
|
||||
"Arnassi Ruins, Song Plant Spore",
|
||||
"Arnassi Ruins, Arnassi Armor",
|
||||
"Arnassi Ruins, Arnassi Statue",
|
||||
"Arnassi Ruins, Transturtle",
|
||||
"Arnassi Ruins, Crab Armor",
|
||||
"Simon Says area, Transturtle",
|
||||
"Mithalas City, first bulb in the left city part",
|
||||
"Mithalas City, second bulb in the left city part",
|
||||
"Mithalas City, bulb in the right part",
|
||||
"Mithalas City, bulb at the top of the city",
|
||||
"Mithalas City, first bulb in a broken home",
|
||||
"Mithalas City, second bulb in a broken home",
|
||||
"Mithalas City, bulb in the bottom left part",
|
||||
"Mithalas City, first bulb in one of the homes",
|
||||
"Mithalas City, second bulb in one of the homes",
|
||||
"Mithalas City, first urn in one of the homes",
|
||||
"Mithalas City, second urn in one of the homes",
|
||||
"Mithalas City, first urn in the city reserve",
|
||||
"Mithalas City, second urn in the city reserve",
|
||||
"Mithalas City, third urn in the city reserve",
|
||||
"Mithalas City, first bulb at the end of the top path",
|
||||
"Mithalas City, second bulb at the end of the top path",
|
||||
"Mithalas City, bulb in the top path",
|
||||
"Mithalas City, Mithalas Pot",
|
||||
"Mithalas City, urn in the Castle flower tube entrance",
|
||||
"Mithalas City, Doll",
|
||||
"Mithalas City, urn inside a home fish pass",
|
||||
"Mithalas City Castle, bulb in the flesh hole",
|
||||
"Mithalas City Castle, Blue Banner",
|
||||
"Mithalas City Castle, urn in the bedroom",
|
||||
"Mithalas City Castle, first urn of the single lamp path",
|
||||
"Mithalas City Castle, second urn of the single lamp path",
|
||||
"Mithalas City Castle, urn in the bottom room",
|
||||
"Mithalas City Castle, first urn on the entrance path",
|
||||
"Mithalas City Castle, second urn on the entrance path",
|
||||
"Mithalas City Castle, beating the Priests",
|
||||
"Mithalas City Castle, Trident Head",
|
||||
"Mithalas Cathedral, first urn in the top right room",
|
||||
"Mithalas Cathedral, second urn in the top right room",
|
||||
"Mithalas Cathedral, third urn in the top right room",
|
||||
"Mithalas Cathedral, urn in the flesh room with fleas",
|
||||
"Mithalas Cathedral, first urn in the bottom right path",
|
||||
"Mithalas Cathedral, second urn in the bottom right path",
|
||||
"Mithalas Cathedral, urn behind the flesh vein",
|
||||
"Mithalas Cathedral, urn in the top left eyes boss room",
|
||||
"Mithalas Cathedral, first urn in the path behind the flesh vein",
|
||||
"Mithalas Cathedral, second urn in the path behind the flesh vein",
|
||||
"Mithalas Cathedral, third urn in the path behind the flesh vein",
|
||||
"Mithalas Cathedral, fourth urn in the top right room",
|
||||
"Mithalas Cathedral, Mithalan Dress",
|
||||
"Mithalas Cathedral, urn below the left entrance",
|
||||
"Cathedral Underground, bulb in the center part",
|
||||
"Cathedral Underground, first bulb in the top left part",
|
||||
"Cathedral Underground, second bulb in the top left part",
|
||||
"Cathedral Underground, third bulb in the top left part",
|
||||
"Cathedral Underground, bulb close to the save crystal",
|
||||
"Cathedral Underground, bulb in the bottom right path",
|
||||
"Mithalas boss area, beating Mithalan God",
|
||||
"Kelp Forest top left area, bulb in the bottom left clearing",
|
||||
"Kelp Forest top left area, bulb in the path down from the top left clearing",
|
||||
"Kelp Forest top left area, bulb in the top left clearing",
|
||||
"Kelp Forest top left area, Jelly Egg",
|
||||
"Kelp Forest top left area, bulb close to the Verse Egg",
|
||||
"Kelp Forest top left area, Verse Egg",
|
||||
"Kelp Forest top right area, bulb under the rock in the right path",
|
||||
"Kelp Forest top right area, bulb at the left of the center clearing",
|
||||
"Kelp Forest top right area, bulb in the left path's big room",
|
||||
"Kelp Forest top right area, bulb in the left path's small room",
|
||||
"Kelp Forest top right area, bulb at the top of the center clearing",
|
||||
"Kelp Forest top right area, Black Pearl",
|
||||
"Kelp Forest top right area, bulb in the top fish pass",
|
||||
"Kelp Forest bottom left area, bulb close to the spirit crystals",
|
||||
"Kelp Forest bottom left area, Walker Baby",
|
||||
"Kelp Forest bottom left area, Transturtle",
|
||||
"Kelp Forest bottom right area, Odd Container",
|
||||
"Kelp Forest boss area, beating Drunian God",
|
||||
"Kelp Forest boss room, bulb at the bottom of the area",
|
||||
"Kelp Forest bottom left area, Fish Cave puzzle",
|
||||
"Kelp Forest sprite cave, bulb inside the fish pass",
|
||||
"Kelp Forest sprite cave, bulb in the second room",
|
||||
"Kelp Forest sprite cave, Seed Bag",
|
||||
"Mermog cave, bulb in the left part of the cave",
|
||||
"Mermog cave, Piranha Egg",
|
||||
"The Veil top left area, In Li's cave",
|
||||
"The Veil top left area, bulb under the rock in the top right path",
|
||||
"The Veil top left area, bulb hidden behind the blocking rock",
|
||||
"The Veil top left area, Transturtle",
|
||||
"The Veil top left area, bulb inside the fish pass",
|
||||
"Turtle cave, Turtle Egg",
|
||||
"Turtle cave, bulb in Bubble Cliff",
|
||||
"Turtle cave, Urchin Costume",
|
||||
"The Veil top right area, bulb in the middle of the wall jump cliff",
|
||||
"The Veil top right area, Golden Starfish",
|
||||
"The Veil top right area, bulb at the top of the waterfall",
|
||||
"The Veil top right area, Transturtle",
|
||||
"The Veil bottom area, bulb in the left path",
|
||||
"The Veil bottom area, bulb in the spirit path",
|
||||
"The Veil bottom area, Verse Egg",
|
||||
"The Veil bottom area, Stone Head",
|
||||
"Octopus Cave, Dumbo Egg",
|
||||
"Octopus Cave, bulb in the path below the Octopus Cave path",
|
||||
"Bubble Cave, bulb in the left cave wall",
|
||||
"Bubble Cave, bulb in the right cave wall (behind the ice crystal)",
|
||||
"Bubble Cave, Verse Egg",
|
||||
"Sun Temple, bulb in the top left part",
|
||||
"Sun Temple, bulb in the top right part",
|
||||
"Sun Temple, bulb at the top of the high dark room",
|
||||
"Sun Temple, Golden Gear",
|
||||
"Sun Temple, first bulb of the temple",
|
||||
"Sun Temple, bulb on the right part",
|
||||
"Sun Temple, bulb in the hidden room of the right part",
|
||||
"Sun Temple, Sun Key",
|
||||
"Sun Worm path, first path bulb",
|
||||
"Sun Worm path, second path bulb",
|
||||
"Sun Worm path, first cliff bulb",
|
||||
"Sun Worm path, second cliff bulb",
|
||||
"Sun Temple boss area, beating Sun God",
|
||||
"Abyss left area, bulb in hidden path room",
|
||||
"Abyss left area, bulb in the right part",
|
||||
"Abyss left area, Glowing Seed",
|
||||
"Abyss left area, Glowing Plant",
|
||||
"Abyss left area, bulb in the bottom fish pass",
|
||||
"Abyss right area, bulb behind the rock in the whale room",
|
||||
"Abyss right area, bulb in the middle path",
|
||||
"Abyss right area, bulb behind the rock in the middle path",
|
||||
"Abyss right area, bulb in the left green room",
|
||||
"Abyss right area, Transturtle",
|
||||
"Ice Cave, bulb in the room to the right",
|
||||
"Ice Cave, first bulb in the top exit room",
|
||||
"Ice Cave, second bulb in the top exit room",
|
||||
"Ice Cave, third bulb in the top exit room",
|
||||
"Ice Cave, bulb in the left room",
|
||||
"King Jellyfish Cave, bulb in the right path from King Jelly",
|
||||
"King Jellyfish Cave, Jellyfish Costume",
|
||||
"The Whale, Verse Egg",
|
||||
"Sunken City right area, crate close to the save crystal",
|
||||
"Sunken City right area, crate in the left bottom room",
|
||||
"Sunken City left area, crate in the little pipe room",
|
||||
"Sunken City left area, crate close to the save crystal",
|
||||
"Sunken City left area, crate before the bedroom",
|
||||
"Sunken City left area, Girl Costume",
|
||||
"Sunken City, bulb on top of the boss area",
|
||||
"The Body center area, breaking Li's cage",
|
||||
"The Body center area, bulb on the main path blocking tube",
|
||||
"The Body left area, first bulb in the top face room",
|
||||
"The Body left area, second bulb in the top face room",
|
||||
"The Body left area, bulb below the water stream",
|
||||
"The Body left area, bulb in the top path to the top face room",
|
||||
"The Body left area, bulb in the bottom face room",
|
||||
"The Body right area, bulb in the top face room",
|
||||
"The Body right area, bulb in the top path to the bottom face room",
|
||||
"The Body right area, bulb in the bottom face room",
|
||||
"The Body bottom area, bulb in the Jelly Zap room",
|
||||
"The Body bottom area, bulb in the nautilus room",
|
||||
"The Body bottom area, Mutant Costume",
|
||||
"Final Boss area, first bulb in the turtle room",
|
||||
"Final Boss area, second bulb in the turtle room",
|
||||
"Final Boss area, third bulb in the turtle room",
|
||||
"Final Boss area, Transturtle",
|
||||
"Final Boss area, bulb in the boss third form room",
|
||||
"Simon Says area, beating Simon Says",
|
||||
"Beating Fallen God",
|
||||
"Beating Mithalan God",
|
||||
"Beating Drunian God",
|
||||
"Beating Sun God",
|
||||
"Beating the Golem",
|
||||
"Beating Nautilus Prime",
|
||||
"Beating Blaster Peg Prime",
|
||||
"Beating Mergog",
|
||||
"Beating Mithalan priests",
|
||||
"Beating Octopus Prime",
|
||||
"Beating Crabbius Maximus",
|
||||
"Beating Mantis Shrimp Prime",
|
||||
"Beating King Jellyfish God Prime",
|
||||
"First secret",
|
||||
"Second secret",
|
||||
"Third secret",
|
||||
"Sunken City cleared",
|
||||
"Objective complete",
|
||||
AquariaLocationNames.SUN_CRYSTAL,
|
||||
AquariaLocationNames.HOME_WATERS_TRANSTURTLE,
|
||||
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_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_TOP_RIGHT_AREA_BULB_IN_THE_TURTLE_ROOM,
|
||||
AquariaLocationNames.OPEN_WATERS_TOP_RIGHT_AREA_TRANSTURTLE,
|
||||
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_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_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_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.MITHALAS_CATHEDRAL_BULB_IN_THE_FLESH_ROOM_WITH_FLEAS,
|
||||
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_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,
|
||||
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.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.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,
|
||||
AquariaLocationNames.ABYSS_LEFT_AREA_BULB_IN_HIDDEN_PATH_ROOM,
|
||||
AquariaLocationNames.ABYSS_LEFT_AREA_BULB_IN_THE_RIGHT_PART,
|
||||
AquariaLocationNames.ABYSS_LEFT_AREA_GLOWING_SEED,
|
||||
AquariaLocationNames.ABYSS_LEFT_AREA_GLOWING_PLANT,
|
||||
AquariaLocationNames.ABYSS_LEFT_AREA_BULB_IN_THE_BOTTOM_FISH_PASS,
|
||||
AquariaLocationNames.ABYSS_RIGHT_AREA_BULB_BEHIND_THE_ROCK_IN_THE_WHALE_ROOM,
|
||||
AquariaLocationNames.ABYSS_RIGHT_AREA_BULB_IN_THE_MIDDLE_PATH,
|
||||
AquariaLocationNames.ABYSS_RIGHT_AREA_BULB_BEHIND_THE_ROCK_IN_THE_MIDDLE_PATH,
|
||||
AquariaLocationNames.ABYSS_RIGHT_AREA_BULB_IN_THE_LEFT_GREEN_ROOM,
|
||||
AquariaLocationNames.ABYSS_RIGHT_AREA_TRANSTURTLE,
|
||||
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.KING_JELLYFISH_CAVE_BULB_IN_THE_RIGHT_PATH_FROM_KING_JELLY,
|
||||
AquariaLocationNames.KING_JELLYFISH_CAVE_JELLYFISH_COSTUME,
|
||||
AquariaLocationNames.THE_WHALE_VERSE_EGG,
|
||||
AquariaLocationNames.SUNKEN_CITY_RIGHT_AREA_CRATE_CLOSE_TO_THE_SAVE_CRYSTAL,
|
||||
AquariaLocationNames.SUNKEN_CITY_RIGHT_AREA_CRATE_IN_THE_LEFT_BOTTOM_ROOM,
|
||||
AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_CRATE_IN_THE_LITTLE_PIPE_ROOM,
|
||||
AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_CRATE_CLOSE_TO_THE_SAVE_CRYSTAL,
|
||||
AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_CRATE_BEFORE_THE_BEDROOM,
|
||||
AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_GIRL_COSTUME,
|
||||
AquariaLocationNames.SUNKEN_CITY_BULB_ON_TOP_OF_THE_BOSS_AREA,
|
||||
AquariaLocationNames.THE_BODY_CENTER_AREA_BREAKING_LI_S_CAGE,
|
||||
AquariaLocationNames.THE_BODY_CENTER_AREA_BULB_ON_THE_MAIN_PATH_BLOCKING_TUBE,
|
||||
AquariaLocationNames.THE_BODY_LEFT_AREA_FIRST_BULB_IN_THE_TOP_FACE_ROOM,
|
||||
AquariaLocationNames.THE_BODY_LEFT_AREA_SECOND_BULB_IN_THE_TOP_FACE_ROOM,
|
||||
AquariaLocationNames.THE_BODY_LEFT_AREA_BULB_BELOW_THE_WATER_STREAM,
|
||||
AquariaLocationNames.THE_BODY_LEFT_AREA_BULB_IN_THE_TOP_PATH_TO_THE_TOP_FACE_ROOM,
|
||||
AquariaLocationNames.THE_BODY_LEFT_AREA_BULB_IN_THE_BOTTOM_FACE_ROOM,
|
||||
AquariaLocationNames.THE_BODY_RIGHT_AREA_BULB_IN_THE_TOP_FACE_ROOM,
|
||||
AquariaLocationNames.THE_BODY_RIGHT_AREA_BULB_IN_THE_TOP_PATH_TO_THE_BOTTOM_FACE_ROOM,
|
||||
AquariaLocationNames.THE_BODY_RIGHT_AREA_BULB_IN_THE_BOTTOM_FACE_ROOM,
|
||||
AquariaLocationNames.THE_BODY_BOTTOM_AREA_BULB_IN_THE_JELLY_ZAP_ROOM,
|
||||
AquariaLocationNames.THE_BODY_BOTTOM_AREA_BULB_IN_THE_NAUTILUS_ROOM,
|
||||
AquariaLocationNames.THE_BODY_BOTTOM_AREA_MUTANT_COSTUME,
|
||||
AquariaLocationNames.FINAL_BOSS_AREA_FIRST_BULB_IN_THE_TURTLE_ROOM,
|
||||
AquariaLocationNames.FINAL_BOSS_AREA_SECOND_BULB_IN_THE_TURTLE_ROOM,
|
||||
AquariaLocationNames.FINAL_BOSS_AREA_THIRD_BULB_IN_THE_TURTLE_ROOM,
|
||||
AquariaLocationNames.FINAL_BOSS_AREA_TRANSTURTLE,
|
||||
AquariaLocationNames.FINAL_BOSS_AREA_BULB_IN_THE_BOSS_THIRD_FORM_ROOM,
|
||||
AquariaLocationNames.SIMON_SAYS_AREA_BEATING_SIMON_SAYS,
|
||||
AquariaLocationNames.BEATING_FALLEN_GOD,
|
||||
AquariaLocationNames.BEATING_MITHALAN_GOD,
|
||||
AquariaLocationNames.BEATING_DRUNIAN_GOD,
|
||||
AquariaLocationNames.BEATING_LUMEREAN_GOD,
|
||||
AquariaLocationNames.BEATING_THE_GOLEM,
|
||||
AquariaLocationNames.BEATING_NAUTILUS_PRIME,
|
||||
AquariaLocationNames.BEATING_BLASTER_PEG_PRIME,
|
||||
AquariaLocationNames.BEATING_MERGOG,
|
||||
AquariaLocationNames.BEATING_MITHALAN_PRIESTS,
|
||||
AquariaLocationNames.BEATING_OCTOPUS_PRIME,
|
||||
AquariaLocationNames.BEATING_CRABBIUS_MAXIMUS,
|
||||
AquariaLocationNames.BEATING_MANTIS_SHRIMP_PRIME,
|
||||
AquariaLocationNames.BEATING_KING_JELLYFISH_GOD_PRIME,
|
||||
AquariaLocationNames.FIRST_SECRET,
|
||||
AquariaLocationNames.SECOND_SECRET,
|
||||
AquariaLocationNames.THIRD_SECRET,
|
||||
AquariaLocationNames.SUNKEN_CITY_CLEARED,
|
||||
AquariaLocationNames.OBJECTIVE_COMPLETE,
|
||||
]
|
||||
|
||||
class AquariaTestBase(WorldTestBase):
|
||||
|
||||
@@ -5,6 +5,8 @@ Description: Unit test used to test accessibility of locations with and without
|
||||
"""
|
||||
|
||||
from . import AquariaTestBase
|
||||
from ..Items import ItemNames
|
||||
from ..Locations import AquariaLocationNames
|
||||
|
||||
|
||||
class BeastFormAccessTest(AquariaTestBase):
|
||||
@@ -13,16 +15,16 @@ class BeastFormAccessTest(AquariaTestBase):
|
||||
def test_beast_form_location(self) -> None:
|
||||
"""Test locations that require beast form"""
|
||||
locations = [
|
||||
"Mermog cave, Piranha Egg",
|
||||
"Kelp Forest top left area, Jelly Egg",
|
||||
"Mithalas Cathedral, Mithalan Dress",
|
||||
"The Veil top right area, bulb at the top of the waterfall",
|
||||
"Sunken City, bulb on top of the boss area",
|
||||
"Octopus Cave, Dumbo Egg",
|
||||
"Beating the Golem",
|
||||
"Beating Mergog",
|
||||
"Beating Octopus Prime",
|
||||
"Sunken City cleared",
|
||||
AquariaLocationNames.MERMOG_CAVE_PIRANHA_EGG,
|
||||
AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_JELLY_EGG,
|
||||
AquariaLocationNames.MITHALAS_CATHEDRAL_MITHALAN_DRESS,
|
||||
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.BEATING_THE_GOLEM,
|
||||
AquariaLocationNames.BEATING_MERGOG,
|
||||
AquariaLocationNames.BEATING_OCTOPUS_PRIME,
|
||||
AquariaLocationNames.SUNKEN_CITY_CLEARED,
|
||||
]
|
||||
items = [["Beast form"]]
|
||||
items = [[ItemNames.BEAST_FORM]]
|
||||
self.assertAccessDependency(locations, items)
|
||||
|
||||
@@ -5,6 +5,8 @@ Description: Unit test used to test accessibility of locations with and without
|
||||
"""
|
||||
|
||||
from . import AquariaTestBase
|
||||
from ..Items import ItemNames
|
||||
from ..Locations import AquariaLocationNames
|
||||
|
||||
|
||||
class BeastForArnassiArmormAccessTest(AquariaTestBase):
|
||||
@@ -13,27 +15,27 @@ class BeastForArnassiArmormAccessTest(AquariaTestBase):
|
||||
def test_beast_form_arnassi_armor_location(self) -> None:
|
||||
"""Test locations that require beast form or arnassi armor"""
|
||||
locations = [
|
||||
"Mithalas City Castle, beating the Priests",
|
||||
"Arnassi Ruins, Crab Armor",
|
||||
"Arnassi Ruins, Song Plant Spore",
|
||||
"Mithalas City, first bulb at the end of the top path",
|
||||
"Mithalas City, second bulb at the end of the top path",
|
||||
"Mithalas City, bulb in the top path",
|
||||
"Mithalas City, Mithalas Pot",
|
||||
"Mithalas City, urn in the Castle flower tube entrance",
|
||||
"Mermog cave, Piranha Egg",
|
||||
"Mithalas Cathedral, Mithalan Dress",
|
||||
"Kelp Forest top left area, Jelly Egg",
|
||||
"The Veil top right area, bulb in the middle of the wall jump cliff",
|
||||
"The Veil top right area, bulb at the top of the waterfall",
|
||||
"Sunken City, bulb on top of the boss area",
|
||||
"Octopus Cave, Dumbo Egg",
|
||||
"Beating the Golem",
|
||||
"Beating Mergog",
|
||||
"Beating Crabbius Maximus",
|
||||
"Beating Octopus Prime",
|
||||
"Beating Mithalan priests",
|
||||
"Sunken City cleared"
|
||||
AquariaLocationNames.MITHALAS_CITY_CASTLE_BEATING_THE_PRIESTS,
|
||||
AquariaLocationNames.ARNASSI_RUINS_CRAB_ARMOR,
|
||||
AquariaLocationNames.ARNASSI_RUINS_SONG_PLANT_SPORE,
|
||||
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.MERMOG_CAVE_PIRANHA_EGG,
|
||||
AquariaLocationNames.MITHALAS_CATHEDRAL_MITHALAN_DRESS,
|
||||
AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_JELLY_EGG,
|
||||
AquariaLocationNames.THE_VEIL_TOP_RIGHT_AREA_BULB_IN_THE_MIDDLE_OF_THE_WALL_JUMP_CLIFF,
|
||||
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.BEATING_THE_GOLEM,
|
||||
AquariaLocationNames.BEATING_MERGOG,
|
||||
AquariaLocationNames.BEATING_CRABBIUS_MAXIMUS,
|
||||
AquariaLocationNames.BEATING_OCTOPUS_PRIME,
|
||||
AquariaLocationNames.BEATING_MITHALAN_PRIESTS,
|
||||
AquariaLocationNames.SUNKEN_CITY_CLEARED
|
||||
]
|
||||
items = [["Beast form", "Arnassi Armor"]]
|
||||
items = [[ItemNames.BEAST_FORM, ItemNames.ARNASSI_ARMOR]]
|
||||
self.assertAccessDependency(locations, items)
|
||||
|
||||
@@ -6,31 +6,36 @@ Description: Unit test used to test accessibility of locations with and without
|
||||
"""
|
||||
|
||||
from . import AquariaTestBase, after_home_water_locations
|
||||
from ..Items import ItemNames
|
||||
from ..Locations import AquariaLocationNames
|
||||
from ..Options import UnconfineHomeWater, EarlyBindSong
|
||||
|
||||
|
||||
class BindSongAccessTest(AquariaTestBase):
|
||||
"""Unit test used to test accessibility of locations with and without the bind song"""
|
||||
options = {
|
||||
"bind_song_needed_to_get_under_rock_bulb": False,
|
||||
"unconfine_home_water": UnconfineHomeWater.option_off,
|
||||
"early_bind_song": EarlyBindSong.option_off
|
||||
}
|
||||
|
||||
def test_bind_song_location(self) -> None:
|
||||
"""Test locations that require Bind song"""
|
||||
locations = [
|
||||
"Verse Cave right area, Big Seed",
|
||||
"Home Water, bulb in the path below Nautilus Prime",
|
||||
"Home Water, bulb in the bottom left room",
|
||||
"Home Water, Nautilus Egg",
|
||||
"Song Cave, Verse Egg",
|
||||
"Energy Temple first area, beating the Energy Statue",
|
||||
"Energy Temple first area, bulb in the bottom room blocked by a rock",
|
||||
"Energy Temple first area, Energy Idol",
|
||||
"Energy Temple second area, bulb under the rock",
|
||||
"Energy Temple bottom entrance, Krotite Armor",
|
||||
"Energy Temple third area, bulb in the bottom path",
|
||||
"Energy Temple boss area, Fallen God Tooth",
|
||||
"Energy Temple blaster room, Blaster Egg",
|
||||
AquariaLocationNames.VERSE_CAVE_RIGHT_AREA_BIG_SEED,
|
||||
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.SONG_CAVE_VERSE_EGG,
|
||||
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,
|
||||
*after_home_water_locations
|
||||
]
|
||||
items = [["Bind song"]]
|
||||
items = [[ItemNames.BIND_SONG]]
|
||||
self.assertAccessDependency(locations, items)
|
||||
|
||||
@@ -7,6 +7,8 @@ Description: Unit test used to test accessibility of locations with and without
|
||||
|
||||
from . import AquariaTestBase
|
||||
from .test_bind_song_access import after_home_water_locations
|
||||
from ..Items import ItemNames
|
||||
from ..Locations import AquariaLocationNames
|
||||
|
||||
|
||||
class BindSongOptionAccessTest(AquariaTestBase):
|
||||
@@ -18,25 +20,25 @@ class BindSongOptionAccessTest(AquariaTestBase):
|
||||
def test_bind_song_location(self) -> None:
|
||||
"""Test locations that require Bind song with the bind song needed option activated"""
|
||||
locations = [
|
||||
"Verse Cave right area, Big Seed",
|
||||
"Verse Cave left area, bulb under the rock at the end of the path",
|
||||
"Home Water, bulb under the rock in the left path from the Verse Cave",
|
||||
"Song Cave, bulb under the rock close to the song door",
|
||||
"Song Cave, bulb under the rock in the path to the singing statues",
|
||||
"Naija's Home, bulb under the rock at the right of the main path",
|
||||
"Home Water, bulb in the path below Nautilus Prime",
|
||||
"Home Water, bulb in the bottom left room",
|
||||
"Home Water, Nautilus Egg",
|
||||
"Song Cave, Verse Egg",
|
||||
"Energy Temple first area, beating the Energy Statue",
|
||||
"Energy Temple first area, bulb in the bottom room blocked by a rock",
|
||||
"Energy Temple first area, Energy Idol",
|
||||
"Energy Temple second area, bulb under the rock",
|
||||
"Energy Temple bottom entrance, Krotite Armor",
|
||||
"Energy Temple third area, bulb in the bottom path",
|
||||
"Energy Temple boss area, Fallen God Tooth",
|
||||
"Energy Temple blaster room, Blaster Egg",
|
||||
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.SONG_CAVE_BULB_UNDER_THE_ROCK_CLOSE_TO_THE_SONG_DOOR,
|
||||
AquariaLocationNames.SONG_CAVE_BULB_UNDER_THE_ROCK_IN_THE_PATH_TO_THE_SINGING_STATUES,
|
||||
AquariaLocationNames.NAIJA_S_HOME_BULB_UNDER_THE_ROCK_AT_THE_RIGHT_OF_THE_MAIN_PATH,
|
||||
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.SONG_CAVE_VERSE_EGG,
|
||||
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,
|
||||
*after_home_water_locations
|
||||
]
|
||||
items = [["Bind song"]]
|
||||
items = [[ItemNames.BIND_SONG]]
|
||||
self.assertAccessDependency(locations, items)
|
||||
|
||||
@@ -5,16 +5,17 @@ Description: Unit test used to test accessibility of region with the home water
|
||||
"""
|
||||
|
||||
from . import AquariaTestBase
|
||||
from ..Options import UnconfineHomeWater, EarlyEnergyForm
|
||||
|
||||
|
||||
class ConfinedHomeWaterAccessTest(AquariaTestBase):
|
||||
"""Unit test used to test accessibility of region with the unconfine home water option disabled"""
|
||||
options = {
|
||||
"unconfine_home_water": 0,
|
||||
"early_energy_form": False
|
||||
"unconfine_home_water": UnconfineHomeWater.option_off,
|
||||
"early_energy_form": EarlyEnergyForm.option_off
|
||||
}
|
||||
|
||||
def test_confine_home_water_location(self) -> None:
|
||||
"""Test region accessible with confined home water"""
|
||||
self.assertFalse(self.can_reach_region("Open Water top left area"), "Can reach Open Water top left area")
|
||||
self.assertFalse(self.can_reach_region("Home Water, turtle room"), "Can reach Home Water, turtle room")
|
||||
self.assertFalse(self.can_reach_region("Open Waters top left area"), "Can reach Open Waters top left area")
|
||||
self.assertFalse(self.can_reach_region("Home Waters, turtle room"), "Can reach Home Waters, turtle room")
|
||||
|
||||
@@ -5,22 +5,25 @@ Description: Unit test used to test accessibility of locations with and without
|
||||
"""
|
||||
|
||||
from . import AquariaTestBase
|
||||
from ..Items import ItemNames
|
||||
from ..Locations import AquariaLocationNames
|
||||
from ..Options import TurtleRandomizer
|
||||
|
||||
|
||||
class LiAccessTest(AquariaTestBase):
|
||||
"""Unit test used to test accessibility of locations with and without the dual song"""
|
||||
options = {
|
||||
"turtle_randomizer": 1,
|
||||
"turtle_randomizer": TurtleRandomizer.option_all,
|
||||
}
|
||||
|
||||
def test_li_song_location(self) -> None:
|
||||
"""Test locations that require the dual song"""
|
||||
locations = [
|
||||
"The Body bottom area, bulb in the Jelly Zap room",
|
||||
"The Body bottom area, bulb in the nautilus room",
|
||||
"The Body bottom area, Mutant Costume",
|
||||
"Final Boss area, bulb in the boss third form room",
|
||||
"Objective complete"
|
||||
AquariaLocationNames.THE_BODY_BOTTOM_AREA_BULB_IN_THE_JELLY_ZAP_ROOM,
|
||||
AquariaLocationNames.THE_BODY_BOTTOM_AREA_BULB_IN_THE_NAUTILUS_ROOM,
|
||||
AquariaLocationNames.THE_BODY_BOTTOM_AREA_MUTANT_COSTUME,
|
||||
AquariaLocationNames.FINAL_BOSS_AREA_BULB_IN_THE_BOSS_THIRD_FORM_ROOM,
|
||||
AquariaLocationNames.OBJECTIVE_COMPLETE
|
||||
]
|
||||
items = [["Dual form"]]
|
||||
items = [[ItemNames.DUAL_FORM]]
|
||||
self.assertAccessDependency(locations, items)
|
||||
|
||||
@@ -6,28 +6,31 @@ Description: Unit test used to test accessibility of locations with and without
|
||||
"""
|
||||
|
||||
from . import AquariaTestBase
|
||||
from ..Items import ItemNames
|
||||
from ..Locations import AquariaLocationNames
|
||||
from ..Options import EarlyEnergyForm
|
||||
|
||||
|
||||
class EnergyFormAccessTest(AquariaTestBase):
|
||||
"""Unit test used to test accessibility of locations with and without the energy form"""
|
||||
options = {
|
||||
"early_energy_form": False,
|
||||
"early_energy_form": EarlyEnergyForm.option_off
|
||||
}
|
||||
|
||||
def test_energy_form_location(self) -> None:
|
||||
"""Test locations that require Energy form"""
|
||||
locations = [
|
||||
"Energy Temple second area, bulb under the rock",
|
||||
"Energy Temple third area, bulb in the bottom path",
|
||||
"The Body left area, first bulb in the top face room",
|
||||
"The Body left area, second bulb in the top face room",
|
||||
"The Body left area, bulb below the water stream",
|
||||
"The Body left area, bulb in the top path to the top face room",
|
||||
"The Body left area, bulb in the bottom face room",
|
||||
"The Body right area, bulb in the top path to the bottom face room",
|
||||
"The Body right area, bulb in the bottom face room",
|
||||
"Final Boss area, bulb in the boss third form room",
|
||||
"Objective complete",
|
||||
AquariaLocationNames.ENERGY_TEMPLE_SECOND_AREA_BULB_UNDER_THE_ROCK,
|
||||
AquariaLocationNames.ENERGY_TEMPLE_THIRD_AREA_BULB_IN_THE_BOTTOM_PATH,
|
||||
AquariaLocationNames.THE_BODY_LEFT_AREA_FIRST_BULB_IN_THE_TOP_FACE_ROOM,
|
||||
AquariaLocationNames.THE_BODY_LEFT_AREA_SECOND_BULB_IN_THE_TOP_FACE_ROOM,
|
||||
AquariaLocationNames.THE_BODY_LEFT_AREA_BULB_BELOW_THE_WATER_STREAM,
|
||||
AquariaLocationNames.THE_BODY_LEFT_AREA_BULB_IN_THE_TOP_PATH_TO_THE_TOP_FACE_ROOM,
|
||||
AquariaLocationNames.THE_BODY_LEFT_AREA_BULB_IN_THE_BOTTOM_FACE_ROOM,
|
||||
AquariaLocationNames.THE_BODY_RIGHT_AREA_BULB_IN_THE_TOP_PATH_TO_THE_BOTTOM_FACE_ROOM,
|
||||
AquariaLocationNames.THE_BODY_RIGHT_AREA_BULB_IN_THE_BOTTOM_FACE_ROOM,
|
||||
AquariaLocationNames.FINAL_BOSS_AREA_BULB_IN_THE_BOSS_THIRD_FORM_ROOM,
|
||||
AquariaLocationNames.OBJECTIVE_COMPLETE,
|
||||
]
|
||||
items = [["Energy form"]]
|
||||
items = [[ItemNames.ENERGY_FORM]]
|
||||
self.assertAccessDependency(locations, items)
|
||||
|
||||
@@ -5,88 +5,74 @@ Description: Unit test used to test accessibility of locations with and without
|
||||
"""
|
||||
|
||||
from . import AquariaTestBase
|
||||
from ..Items import ItemNames
|
||||
from ..Locations import AquariaLocationNames
|
||||
from ..Options import EarlyEnergyForm, TurtleRandomizer
|
||||
|
||||
|
||||
class EnergyFormDualFormAccessTest(AquariaTestBase):
|
||||
"""Unit test used to test accessibility of locations with and without the energy form and dual form (and Li)"""
|
||||
options = {
|
||||
"early_energy_form": False,
|
||||
"early_energy_form": EarlyEnergyForm.option_off,
|
||||
"turtle_randomizer": TurtleRandomizer.option_all
|
||||
}
|
||||
|
||||
def test_energy_form_or_dual_form_location(self) -> None:
|
||||
"""Test locations that require Energy form or dual form"""
|
||||
locations = [
|
||||
"Naija's Home, bulb after the energy door",
|
||||
"Home Water, Nautilus Egg",
|
||||
"Energy Temple second area, bulb under the rock",
|
||||
"Energy Temple bottom entrance, Krotite Armor",
|
||||
"Energy Temple third area, bulb in the bottom path",
|
||||
"Energy Temple blaster room, Blaster Egg",
|
||||
"Energy Temple boss area, Fallen God Tooth",
|
||||
"Mithalas City Castle, beating the Priests",
|
||||
"Mithalas boss area, beating Mithalan God",
|
||||
"Mithalas Cathedral, first urn in the top right room",
|
||||
"Mithalas Cathedral, second urn in the top right room",
|
||||
"Mithalas Cathedral, third urn in the top right room",
|
||||
"Mithalas Cathedral, urn in the flesh room with fleas",
|
||||
"Mithalas Cathedral, first urn in the bottom right path",
|
||||
"Mithalas Cathedral, second urn in the bottom right path",
|
||||
"Mithalas Cathedral, urn behind the flesh vein",
|
||||
"Mithalas Cathedral, urn in the top left eyes boss room",
|
||||
"Mithalas Cathedral, first urn in the path behind the flesh vein",
|
||||
"Mithalas Cathedral, second urn in the path behind the flesh vein",
|
||||
"Mithalas Cathedral, third urn in the path behind the flesh vein",
|
||||
"Mithalas Cathedral, fourth urn in the top right room",
|
||||
"Mithalas Cathedral, Mithalan Dress",
|
||||
"Mithalas Cathedral, urn below the left entrance",
|
||||
"Kelp Forest top left area, bulb close to the Verse Egg",
|
||||
"Kelp Forest top left area, Verse Egg",
|
||||
"Kelp Forest boss area, beating Drunian God",
|
||||
"Mermog cave, Piranha Egg",
|
||||
"Octopus Cave, Dumbo Egg",
|
||||
"Sun Temple boss area, beating Sun God",
|
||||
"King Jellyfish Cave, bulb in the right path from King Jelly",
|
||||
"King Jellyfish Cave, Jellyfish Costume",
|
||||
"Sunken City right area, crate close to the save crystal",
|
||||
"Sunken City right area, crate in the left bottom room",
|
||||
"Sunken City left area, crate in the little pipe room",
|
||||
"Sunken City left area, crate close to the save crystal",
|
||||
"Sunken City left area, crate before the bedroom",
|
||||
"Sunken City left area, Girl Costume",
|
||||
"Sunken City, bulb on top of the boss area",
|
||||
"The Body center area, breaking Li's cage",
|
||||
"The Body center area, bulb on the main path blocking tube",
|
||||
"The Body left area, first bulb in the top face room",
|
||||
"The Body left area, second bulb in the top face room",
|
||||
"The Body left area, bulb below the water stream",
|
||||
"The Body left area, bulb in the top path to the top face room",
|
||||
"The Body left area, bulb in the bottom face room",
|
||||
"The Body right area, bulb in the top face room",
|
||||
"The Body right area, bulb in the top path to the bottom face room",
|
||||
"The Body right area, bulb in the bottom face room",
|
||||
"The Body bottom area, bulb in the Jelly Zap room",
|
||||
"The Body bottom area, bulb in the nautilus room",
|
||||
"The Body bottom area, Mutant Costume",
|
||||
"Final Boss area, bulb in the boss third form room",
|
||||
"Final Boss area, first bulb in the turtle room",
|
||||
"Final Boss area, second bulb in the turtle room",
|
||||
"Final Boss area, third bulb in the turtle room",
|
||||
"Final Boss area, Transturtle",
|
||||
"Beating Fallen God",
|
||||
"Beating Blaster Peg Prime",
|
||||
"Beating Mithalan God",
|
||||
"Beating Drunian God",
|
||||
"Beating Sun God",
|
||||
"Beating the Golem",
|
||||
"Beating Nautilus Prime",
|
||||
"Beating Mergog",
|
||||
"Beating Mithalan priests",
|
||||
"Beating Octopus Prime",
|
||||
"Beating King Jellyfish God Prime",
|
||||
"Beating the Golem",
|
||||
"Sunken City cleared",
|
||||
"First secret",
|
||||
"Objective complete"
|
||||
AquariaLocationNames.NAIJA_S_HOME_BULB_AFTER_THE_ENERGY_DOOR,
|
||||
AquariaLocationNames.HOME_WATERS_NAUTILUS_EGG,
|
||||
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_BLASTER_ROOM_BLASTER_EGG,
|
||||
AquariaLocationNames.ENERGY_TEMPLE_BOSS_AREA_FALLEN_GOD_TOOTH,
|
||||
AquariaLocationNames.MITHALAS_CITY_CASTLE_BEATING_THE_PRIESTS,
|
||||
AquariaLocationNames.MITHALAS_BOSS_AREA_BEATING_MITHALAN_GOD,
|
||||
AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_BULB_CLOSE_TO_THE_VERSE_EGG,
|
||||
AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_VERSE_EGG,
|
||||
AquariaLocationNames.KELP_FOREST_BOSS_AREA_BEATING_DRUNIAN_GOD,
|
||||
AquariaLocationNames.MERMOG_CAVE_PIRANHA_EGG,
|
||||
AquariaLocationNames.OCTOPUS_CAVE_DUMBO_EGG,
|
||||
AquariaLocationNames.SUN_TEMPLE_BOSS_AREA_BEATING_LUMEREAN_GOD,
|
||||
AquariaLocationNames.KING_JELLYFISH_CAVE_BULB_IN_THE_RIGHT_PATH_FROM_KING_JELLY,
|
||||
AquariaLocationNames.KING_JELLYFISH_CAVE_JELLYFISH_COSTUME,
|
||||
AquariaLocationNames.SUNKEN_CITY_RIGHT_AREA_CRATE_CLOSE_TO_THE_SAVE_CRYSTAL,
|
||||
AquariaLocationNames.SUNKEN_CITY_RIGHT_AREA_CRATE_IN_THE_LEFT_BOTTOM_ROOM,
|
||||
AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_CRATE_IN_THE_LITTLE_PIPE_ROOM,
|
||||
AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_CRATE_CLOSE_TO_THE_SAVE_CRYSTAL,
|
||||
AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_CRATE_BEFORE_THE_BEDROOM,
|
||||
AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_GIRL_COSTUME,
|
||||
AquariaLocationNames.SUNKEN_CITY_BULB_ON_TOP_OF_THE_BOSS_AREA,
|
||||
AquariaLocationNames.THE_BODY_CENTER_AREA_BREAKING_LI_S_CAGE,
|
||||
AquariaLocationNames.THE_BODY_CENTER_AREA_BULB_ON_THE_MAIN_PATH_BLOCKING_TUBE,
|
||||
AquariaLocationNames.THE_BODY_LEFT_AREA_FIRST_BULB_IN_THE_TOP_FACE_ROOM,
|
||||
AquariaLocationNames.THE_BODY_LEFT_AREA_SECOND_BULB_IN_THE_TOP_FACE_ROOM,
|
||||
AquariaLocationNames.THE_BODY_LEFT_AREA_BULB_BELOW_THE_WATER_STREAM,
|
||||
AquariaLocationNames.THE_BODY_LEFT_AREA_BULB_IN_THE_TOP_PATH_TO_THE_TOP_FACE_ROOM,
|
||||
AquariaLocationNames.THE_BODY_LEFT_AREA_BULB_IN_THE_BOTTOM_FACE_ROOM,
|
||||
AquariaLocationNames.THE_BODY_RIGHT_AREA_BULB_IN_THE_TOP_FACE_ROOM,
|
||||
AquariaLocationNames.THE_BODY_RIGHT_AREA_BULB_IN_THE_TOP_PATH_TO_THE_BOTTOM_FACE_ROOM,
|
||||
AquariaLocationNames.THE_BODY_RIGHT_AREA_BULB_IN_THE_BOTTOM_FACE_ROOM,
|
||||
AquariaLocationNames.THE_BODY_BOTTOM_AREA_BULB_IN_THE_JELLY_ZAP_ROOM,
|
||||
AquariaLocationNames.THE_BODY_BOTTOM_AREA_BULB_IN_THE_NAUTILUS_ROOM,
|
||||
AquariaLocationNames.THE_BODY_BOTTOM_AREA_MUTANT_COSTUME,
|
||||
AquariaLocationNames.FINAL_BOSS_AREA_BULB_IN_THE_BOSS_THIRD_FORM_ROOM,
|
||||
AquariaLocationNames.BEATING_FALLEN_GOD,
|
||||
AquariaLocationNames.BEATING_BLASTER_PEG_PRIME,
|
||||
AquariaLocationNames.BEATING_MITHALAN_GOD,
|
||||
AquariaLocationNames.BEATING_DRUNIAN_GOD,
|
||||
AquariaLocationNames.BEATING_LUMEREAN_GOD,
|
||||
AquariaLocationNames.BEATING_THE_GOLEM,
|
||||
AquariaLocationNames.BEATING_NAUTILUS_PRIME,
|
||||
AquariaLocationNames.BEATING_MERGOG,
|
||||
AquariaLocationNames.BEATING_MITHALAN_PRIESTS,
|
||||
AquariaLocationNames.BEATING_OCTOPUS_PRIME,
|
||||
AquariaLocationNames.BEATING_KING_JELLYFISH_GOD_PRIME,
|
||||
AquariaLocationNames.BEATING_THE_GOLEM,
|
||||
AquariaLocationNames.SUNKEN_CITY_CLEARED,
|
||||
AquariaLocationNames.FIRST_SECRET,
|
||||
AquariaLocationNames.OBJECTIVE_COMPLETE
|
||||
]
|
||||
items = [["Energy form", "Dual form", "Li and Li song", "Body tongue cleared"]]
|
||||
items = [[ItemNames.ENERGY_FORM, ItemNames.DUAL_FORM, ItemNames.LI_AND_LI_SONG, ItemNames.BODY_TONGUE_CLEARED]]
|
||||
self.assertAccessDependency(locations, items)
|
||||
|
||||
@@ -5,33 +5,36 @@ Description: Unit test used to test accessibility of locations with and without
|
||||
"""
|
||||
|
||||
from . import AquariaTestBase
|
||||
from ..Items import ItemNames
|
||||
from ..Locations import AquariaLocationNames
|
||||
from ..Options import TurtleRandomizer
|
||||
|
||||
|
||||
class FishFormAccessTest(AquariaTestBase):
|
||||
"""Unit test used to test accessibility of locations with and without the fish form"""
|
||||
options = {
|
||||
"turtle_randomizer": 1,
|
||||
"turtle_randomizer": TurtleRandomizer.option_all,
|
||||
}
|
||||
|
||||
def test_fish_form_location(self) -> None:
|
||||
"""Test locations that require fish form"""
|
||||
locations = [
|
||||
"The Veil top left area, bulb inside the fish pass",
|
||||
"Energy Temple first area, Energy Idol",
|
||||
"Mithalas City, Doll",
|
||||
"Mithalas City, urn inside a home fish pass",
|
||||
"Kelp Forest top right area, bulb in the top fish pass",
|
||||
"The Veil bottom area, Verse Egg",
|
||||
"Open Water bottom left area, bulb inside the lowest fish pass",
|
||||
"Kelp Forest top left area, bulb close to the Verse Egg",
|
||||
"Kelp Forest top left area, Verse Egg",
|
||||
"Mermog cave, bulb in the left part of the cave",
|
||||
"Mermog cave, Piranha Egg",
|
||||
"Beating Mergog",
|
||||
"Octopus Cave, Dumbo Egg",
|
||||
"Octopus Cave, bulb in the path below the Octopus Cave path",
|
||||
"Beating Octopus Prime",
|
||||
"Abyss left area, bulb in the bottom fish pass"
|
||||
AquariaLocationNames.THE_VEIL_TOP_LEFT_AREA_BULB_INSIDE_THE_FISH_PASS,
|
||||
AquariaLocationNames.ENERGY_TEMPLE_ENERGY_IDOL,
|
||||
AquariaLocationNames.MITHALAS_CITY_DOLL,
|
||||
AquariaLocationNames.MITHALAS_CITY_URN_INSIDE_A_HOME_FISH_PASS,
|
||||
AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BULB_IN_THE_TOP_FISH_PASS,
|
||||
AquariaLocationNames.THE_VEIL_BOTTOM_AREA_VERSE_EGG,
|
||||
AquariaLocationNames.OPEN_WATERS_BOTTOM_LEFT_AREA_BULB_INSIDE_THE_LOWEST_FISH_PASS,
|
||||
AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_BULB_CLOSE_TO_THE_VERSE_EGG,
|
||||
AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_VERSE_EGG,
|
||||
AquariaLocationNames.MERMOG_CAVE_BULB_IN_THE_LEFT_PART_OF_THE_CAVE,
|
||||
AquariaLocationNames.MERMOG_CAVE_PIRANHA_EGG,
|
||||
AquariaLocationNames.BEATING_MERGOG,
|
||||
AquariaLocationNames.OCTOPUS_CAVE_DUMBO_EGG,
|
||||
AquariaLocationNames.OCTOPUS_CAVE_BULB_IN_THE_PATH_BELOW_THE_OCTOPUS_CAVE_PATH,
|
||||
AquariaLocationNames.BEATING_OCTOPUS_PRIME,
|
||||
AquariaLocationNames.ABYSS_LEFT_AREA_BULB_IN_THE_BOTTOM_FISH_PASS
|
||||
]
|
||||
items = [["Fish form"]]
|
||||
items = [[ItemNames.FISH_FORM]]
|
||||
self.assertAccessDependency(locations, items)
|
||||
|
||||
@@ -5,41 +5,44 @@ Description: Unit test used to test accessibility of locations with and without
|
||||
"""
|
||||
|
||||
from . import AquariaTestBase
|
||||
from ..Items import ItemNames
|
||||
from ..Locations import AquariaLocationNames
|
||||
from ..Options import TurtleRandomizer
|
||||
|
||||
|
||||
class LiAccessTest(AquariaTestBase):
|
||||
"""Unit test used to test accessibility of locations with and without Li"""
|
||||
options = {
|
||||
"turtle_randomizer": 1,
|
||||
"turtle_randomizer": TurtleRandomizer.option_all,
|
||||
}
|
||||
|
||||
def test_li_song_location(self) -> None:
|
||||
"""Test locations that require Li"""
|
||||
locations = [
|
||||
"Sunken City right area, crate close to the save crystal",
|
||||
"Sunken City right area, crate in the left bottom room",
|
||||
"Sunken City left area, crate in the little pipe room",
|
||||
"Sunken City left area, crate close to the save crystal",
|
||||
"Sunken City left area, crate before the bedroom",
|
||||
"Sunken City left area, Girl Costume",
|
||||
"Sunken City, bulb on top of the boss area",
|
||||
"The Body center area, breaking Li's cage",
|
||||
"The Body center area, bulb on the main path blocking tube",
|
||||
"The Body left area, first bulb in the top face room",
|
||||
"The Body left area, second bulb in the top face room",
|
||||
"The Body left area, bulb below the water stream",
|
||||
"The Body left area, bulb in the top path to the top face room",
|
||||
"The Body left area, bulb in the bottom face room",
|
||||
"The Body right area, bulb in the top face room",
|
||||
"The Body right area, bulb in the top path to the bottom face room",
|
||||
"The Body right area, bulb in the bottom face room",
|
||||
"The Body bottom area, bulb in the Jelly Zap room",
|
||||
"The Body bottom area, bulb in the nautilus room",
|
||||
"The Body bottom area, Mutant Costume",
|
||||
"Final Boss area, bulb in the boss third form room",
|
||||
"Beating the Golem",
|
||||
"Sunken City cleared",
|
||||
"Objective complete"
|
||||
AquariaLocationNames.SUNKEN_CITY_RIGHT_AREA_CRATE_CLOSE_TO_THE_SAVE_CRYSTAL,
|
||||
AquariaLocationNames.SUNKEN_CITY_RIGHT_AREA_CRATE_IN_THE_LEFT_BOTTOM_ROOM,
|
||||
AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_CRATE_IN_THE_LITTLE_PIPE_ROOM,
|
||||
AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_CRATE_CLOSE_TO_THE_SAVE_CRYSTAL,
|
||||
AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_CRATE_BEFORE_THE_BEDROOM,
|
||||
AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_GIRL_COSTUME,
|
||||
AquariaLocationNames.SUNKEN_CITY_BULB_ON_TOP_OF_THE_BOSS_AREA,
|
||||
AquariaLocationNames.THE_BODY_CENTER_AREA_BREAKING_LI_S_CAGE,
|
||||
AquariaLocationNames.THE_BODY_CENTER_AREA_BULB_ON_THE_MAIN_PATH_BLOCKING_TUBE,
|
||||
AquariaLocationNames.THE_BODY_LEFT_AREA_FIRST_BULB_IN_THE_TOP_FACE_ROOM,
|
||||
AquariaLocationNames.THE_BODY_LEFT_AREA_SECOND_BULB_IN_THE_TOP_FACE_ROOM,
|
||||
AquariaLocationNames.THE_BODY_LEFT_AREA_BULB_BELOW_THE_WATER_STREAM,
|
||||
AquariaLocationNames.THE_BODY_LEFT_AREA_BULB_IN_THE_TOP_PATH_TO_THE_TOP_FACE_ROOM,
|
||||
AquariaLocationNames.THE_BODY_LEFT_AREA_BULB_IN_THE_BOTTOM_FACE_ROOM,
|
||||
AquariaLocationNames.THE_BODY_RIGHT_AREA_BULB_IN_THE_TOP_FACE_ROOM,
|
||||
AquariaLocationNames.THE_BODY_RIGHT_AREA_BULB_IN_THE_TOP_PATH_TO_THE_BOTTOM_FACE_ROOM,
|
||||
AquariaLocationNames.THE_BODY_RIGHT_AREA_BULB_IN_THE_BOTTOM_FACE_ROOM,
|
||||
AquariaLocationNames.THE_BODY_BOTTOM_AREA_BULB_IN_THE_JELLY_ZAP_ROOM,
|
||||
AquariaLocationNames.THE_BODY_BOTTOM_AREA_BULB_IN_THE_NAUTILUS_ROOM,
|
||||
AquariaLocationNames.THE_BODY_BOTTOM_AREA_MUTANT_COSTUME,
|
||||
AquariaLocationNames.FINAL_BOSS_AREA_BULB_IN_THE_BOSS_THIRD_FORM_ROOM,
|
||||
AquariaLocationNames.BEATING_THE_GOLEM,
|
||||
AquariaLocationNames.SUNKEN_CITY_CLEARED,
|
||||
AquariaLocationNames.OBJECTIVE_COMPLETE
|
||||
]
|
||||
items = [["Li and Li song", "Body tongue cleared"]]
|
||||
items = [[ItemNames.LI_AND_LI_SONG, ItemNames.BODY_TONGUE_CLEARED]]
|
||||
self.assertAccessDependency(locations, items)
|
||||
|
||||
@@ -5,12 +5,15 @@ Description: Unit test used to test accessibility of locations with and without
|
||||
"""
|
||||
|
||||
from . import AquariaTestBase
|
||||
from ..Items import ItemNames
|
||||
from ..Locations import AquariaLocationNames
|
||||
from ..Options import TurtleRandomizer
|
||||
|
||||
|
||||
class LightAccessTest(AquariaTestBase):
|
||||
"""Unit test used to test accessibility of locations with and without light"""
|
||||
options = {
|
||||
"turtle_randomizer": 1,
|
||||
"turtle_randomizer": TurtleRandomizer.option_all,
|
||||
"light_needed_to_get_to_dark_places": True,
|
||||
}
|
||||
|
||||
@@ -19,52 +22,52 @@ class LightAccessTest(AquariaTestBase):
|
||||
locations = [
|
||||
# Since the `assertAccessDependency` sweep for events even if I tell it not to, those location cannot be
|
||||
# tested.
|
||||
# "Third secret",
|
||||
# "Sun Temple, bulb in the top left part",
|
||||
# "Sun Temple, bulb in the top right part",
|
||||
# "Sun Temple, bulb at the top of the high dark room",
|
||||
# "Sun Temple, Golden Gear",
|
||||
# "Sun Worm path, first path bulb",
|
||||
# "Sun Worm path, second path bulb",
|
||||
# "Sun Worm path, first cliff bulb",
|
||||
"Octopus Cave, Dumbo Egg",
|
||||
"Kelp Forest bottom right area, Odd Container",
|
||||
"Kelp Forest top right area, Black Pearl",
|
||||
"Abyss left area, bulb in hidden path room",
|
||||
"Abyss left area, bulb in the right part",
|
||||
"Abyss left area, Glowing Seed",
|
||||
"Abyss left area, Glowing Plant",
|
||||
"Abyss left area, bulb in the bottom fish pass",
|
||||
"Abyss right area, bulb behind the rock in the whale room",
|
||||
"Abyss right area, bulb in the middle path",
|
||||
"Abyss right area, bulb behind the rock in the middle path",
|
||||
"Abyss right area, bulb in the left green room",
|
||||
"Ice Cave, bulb in the room to the right",
|
||||
"Ice Cave, first bulb in the top exit room",
|
||||
"Ice Cave, second bulb in the top exit room",
|
||||
"Ice Cave, third bulb in the top exit room",
|
||||
"Ice Cave, bulb in the left room",
|
||||
"Bubble Cave, bulb in the left cave wall",
|
||||
"Bubble Cave, bulb in the right cave wall (behind the ice crystal)",
|
||||
"Bubble Cave, Verse Egg",
|
||||
"Beating Mantis Shrimp Prime",
|
||||
"King Jellyfish Cave, bulb in the right path from King Jelly",
|
||||
"King Jellyfish Cave, Jellyfish Costume",
|
||||
"Beating King Jellyfish God Prime",
|
||||
"The Whale, Verse Egg",
|
||||
"First secret",
|
||||
"Sunken City right area, crate close to the save crystal",
|
||||
"Sunken City right area, crate in the left bottom room",
|
||||
"Sunken City left area, crate in the little pipe room",
|
||||
"Sunken City left area, crate close to the save crystal",
|
||||
"Sunken City left area, crate before the bedroom",
|
||||
"Sunken City left area, Girl Costume",
|
||||
"Sunken City, bulb on top of the boss area",
|
||||
"Sunken City cleared",
|
||||
"Beating the Golem",
|
||||
"Beating Octopus Prime",
|
||||
"Final Boss area, bulb in the boss third form room",
|
||||
"Objective complete",
|
||||
# AquariaLocationNames.THIRD_SECRET,
|
||||
# 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_BOSS_PATH_FIRST_PATH_BULB,
|
||||
# AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_SECOND_PATH_BULB,
|
||||
# AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_FIRST_CLIFF_BULB,
|
||||
AquariaLocationNames.OCTOPUS_CAVE_DUMBO_EGG,
|
||||
AquariaLocationNames.KELP_FOREST_BOTTOM_RIGHT_AREA_ODD_CONTAINER,
|
||||
AquariaLocationNames.KELP_FOREST_TOP_RIGHT_AREA_BLACK_PEARL,
|
||||
AquariaLocationNames.ABYSS_LEFT_AREA_BULB_IN_HIDDEN_PATH_ROOM,
|
||||
AquariaLocationNames.ABYSS_LEFT_AREA_BULB_IN_THE_RIGHT_PART,
|
||||
AquariaLocationNames.ABYSS_LEFT_AREA_GLOWING_SEED,
|
||||
AquariaLocationNames.ABYSS_LEFT_AREA_GLOWING_PLANT,
|
||||
AquariaLocationNames.ABYSS_LEFT_AREA_BULB_IN_THE_BOTTOM_FISH_PASS,
|
||||
AquariaLocationNames.ABYSS_RIGHT_AREA_BULB_BEHIND_THE_ROCK_IN_THE_WHALE_ROOM,
|
||||
AquariaLocationNames.ABYSS_RIGHT_AREA_BULB_IN_THE_MIDDLE_PATH,
|
||||
AquariaLocationNames.ABYSS_RIGHT_AREA_BULB_BEHIND_THE_ROCK_IN_THE_MIDDLE_PATH,
|
||||
AquariaLocationNames.ABYSS_RIGHT_AREA_BULB_IN_THE_LEFT_GREEN_ROOM,
|
||||
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,
|
||||
AquariaLocationNames.BEATING_MANTIS_SHRIMP_PRIME,
|
||||
AquariaLocationNames.KING_JELLYFISH_CAVE_BULB_IN_THE_RIGHT_PATH_FROM_KING_JELLY,
|
||||
AquariaLocationNames.KING_JELLYFISH_CAVE_JELLYFISH_COSTUME,
|
||||
AquariaLocationNames.BEATING_KING_JELLYFISH_GOD_PRIME,
|
||||
AquariaLocationNames.THE_WHALE_VERSE_EGG,
|
||||
AquariaLocationNames.FIRST_SECRET,
|
||||
AquariaLocationNames.SUNKEN_CITY_RIGHT_AREA_CRATE_CLOSE_TO_THE_SAVE_CRYSTAL,
|
||||
AquariaLocationNames.SUNKEN_CITY_RIGHT_AREA_CRATE_IN_THE_LEFT_BOTTOM_ROOM,
|
||||
AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_CRATE_IN_THE_LITTLE_PIPE_ROOM,
|
||||
AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_CRATE_CLOSE_TO_THE_SAVE_CRYSTAL,
|
||||
AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_CRATE_BEFORE_THE_BEDROOM,
|
||||
AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_GIRL_COSTUME,
|
||||
AquariaLocationNames.SUNKEN_CITY_BULB_ON_TOP_OF_THE_BOSS_AREA,
|
||||
AquariaLocationNames.SUNKEN_CITY_CLEARED,
|
||||
AquariaLocationNames.BEATING_THE_GOLEM,
|
||||
AquariaLocationNames.BEATING_OCTOPUS_PRIME,
|
||||
AquariaLocationNames.FINAL_BOSS_AREA_BULB_IN_THE_BOSS_THIRD_FORM_ROOM,
|
||||
AquariaLocationNames.OBJECTIVE_COMPLETE,
|
||||
]
|
||||
items = [["Sun form", "Baby Dumbo", "Has sun crystal"]]
|
||||
items = [[ItemNames.SUN_FORM, ItemNames.BABY_DUMBO, ItemNames.HAS_SUN_CRYSTAL]]
|
||||
self.assertAccessDependency(locations, items)
|
||||
|
||||
@@ -5,53 +5,56 @@ Description: Unit test used to test accessibility of locations with and without
|
||||
"""
|
||||
|
||||
from . import AquariaTestBase
|
||||
from ..Items import ItemNames
|
||||
from ..Locations import AquariaLocationNames
|
||||
from ..Options import TurtleRandomizer
|
||||
|
||||
|
||||
class NatureFormAccessTest(AquariaTestBase):
|
||||
"""Unit test used to test accessibility of locations with and without the nature form"""
|
||||
options = {
|
||||
"turtle_randomizer": 1,
|
||||
"turtle_randomizer": TurtleRandomizer.option_all,
|
||||
}
|
||||
|
||||
def test_nature_form_location(self) -> None:
|
||||
"""Test locations that require nature form"""
|
||||
locations = [
|
||||
"Song Cave, Anemone Seed",
|
||||
"Energy Temple blaster room, Blaster Egg",
|
||||
"Beating Blaster Peg Prime",
|
||||
"Kelp Forest top left area, Verse Egg",
|
||||
"Kelp Forest top left area, bulb close to the Verse Egg",
|
||||
"Mithalas City Castle, beating the Priests",
|
||||
"Kelp Forest sprite cave, bulb in the second room",
|
||||
"Kelp Forest sprite cave, Seed Bag",
|
||||
"Beating Mithalan priests",
|
||||
"Abyss left area, bulb in the bottom fish pass",
|
||||
"Bubble Cave, Verse Egg",
|
||||
"Beating Mantis Shrimp Prime",
|
||||
"Sunken City right area, crate close to the save crystal",
|
||||
"Sunken City right area, crate in the left bottom room",
|
||||
"Sunken City left area, crate in the little pipe room",
|
||||
"Sunken City left area, crate close to the save crystal",
|
||||
"Sunken City left area, crate before the bedroom",
|
||||
"Sunken City left area, Girl Costume",
|
||||
"Sunken City, bulb on top of the boss area",
|
||||
"Beating the Golem",
|
||||
"Sunken City cleared",
|
||||
"The Body center area, breaking Li's cage",
|
||||
"The Body center area, bulb on the main path blocking tube",
|
||||
"The Body left area, first bulb in the top face room",
|
||||
"The Body left area, second bulb in the top face room",
|
||||
"The Body left area, bulb below the water stream",
|
||||
"The Body left area, bulb in the top path to the top face room",
|
||||
"The Body left area, bulb in the bottom face room",
|
||||
"The Body right area, bulb in the top face room",
|
||||
"The Body right area, bulb in the top path to the bottom face room",
|
||||
"The Body right area, bulb in the bottom face room",
|
||||
"The Body bottom area, bulb in the Jelly Zap room",
|
||||
"The Body bottom area, bulb in the nautilus room",
|
||||
"The Body bottom area, Mutant Costume",
|
||||
"Final Boss area, bulb in the boss third form room",
|
||||
"Objective complete"
|
||||
AquariaLocationNames.SONG_CAVE_ANEMONE_SEED,
|
||||
AquariaLocationNames.ENERGY_TEMPLE_BLASTER_ROOM_BLASTER_EGG,
|
||||
AquariaLocationNames.BEATING_BLASTER_PEG_PRIME,
|
||||
AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_VERSE_EGG,
|
||||
AquariaLocationNames.KELP_FOREST_TOP_LEFT_AREA_BULB_CLOSE_TO_THE_VERSE_EGG,
|
||||
AquariaLocationNames.MITHALAS_CITY_CASTLE_BEATING_THE_PRIESTS,
|
||||
AquariaLocationNames.KELP_FOREST_SPRITE_CAVE_BULB_IN_THE_SECOND_ROOM,
|
||||
AquariaLocationNames.KELP_FOREST_SPRITE_CAVE_SEED_BAG,
|
||||
AquariaLocationNames.BEATING_MITHALAN_PRIESTS,
|
||||
AquariaLocationNames.ABYSS_LEFT_AREA_BULB_IN_THE_BOTTOM_FISH_PASS,
|
||||
AquariaLocationNames.BUBBLE_CAVE_VERSE_EGG,
|
||||
AquariaLocationNames.BEATING_MANTIS_SHRIMP_PRIME,
|
||||
AquariaLocationNames.SUNKEN_CITY_RIGHT_AREA_CRATE_CLOSE_TO_THE_SAVE_CRYSTAL,
|
||||
AquariaLocationNames.SUNKEN_CITY_RIGHT_AREA_CRATE_IN_THE_LEFT_BOTTOM_ROOM,
|
||||
AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_CRATE_IN_THE_LITTLE_PIPE_ROOM,
|
||||
AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_CRATE_CLOSE_TO_THE_SAVE_CRYSTAL,
|
||||
AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_CRATE_BEFORE_THE_BEDROOM,
|
||||
AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_GIRL_COSTUME,
|
||||
AquariaLocationNames.SUNKEN_CITY_BULB_ON_TOP_OF_THE_BOSS_AREA,
|
||||
AquariaLocationNames.BEATING_THE_GOLEM,
|
||||
AquariaLocationNames.SUNKEN_CITY_CLEARED,
|
||||
AquariaLocationNames.THE_BODY_CENTER_AREA_BREAKING_LI_S_CAGE,
|
||||
AquariaLocationNames.THE_BODY_CENTER_AREA_BULB_ON_THE_MAIN_PATH_BLOCKING_TUBE,
|
||||
AquariaLocationNames.THE_BODY_LEFT_AREA_FIRST_BULB_IN_THE_TOP_FACE_ROOM,
|
||||
AquariaLocationNames.THE_BODY_LEFT_AREA_SECOND_BULB_IN_THE_TOP_FACE_ROOM,
|
||||
AquariaLocationNames.THE_BODY_LEFT_AREA_BULB_BELOW_THE_WATER_STREAM,
|
||||
AquariaLocationNames.THE_BODY_LEFT_AREA_BULB_IN_THE_TOP_PATH_TO_THE_TOP_FACE_ROOM,
|
||||
AquariaLocationNames.THE_BODY_LEFT_AREA_BULB_IN_THE_BOTTOM_FACE_ROOM,
|
||||
AquariaLocationNames.THE_BODY_RIGHT_AREA_BULB_IN_THE_TOP_FACE_ROOM,
|
||||
AquariaLocationNames.THE_BODY_RIGHT_AREA_BULB_IN_THE_TOP_PATH_TO_THE_BOTTOM_FACE_ROOM,
|
||||
AquariaLocationNames.THE_BODY_RIGHT_AREA_BULB_IN_THE_BOTTOM_FACE_ROOM,
|
||||
AquariaLocationNames.THE_BODY_BOTTOM_AREA_BULB_IN_THE_JELLY_ZAP_ROOM,
|
||||
AquariaLocationNames.THE_BODY_BOTTOM_AREA_BULB_IN_THE_NAUTILUS_ROOM,
|
||||
AquariaLocationNames.THE_BODY_BOTTOM_AREA_MUTANT_COSTUME,
|
||||
AquariaLocationNames.FINAL_BOSS_AREA_BULB_IN_THE_BOSS_THIRD_FORM_ROOM,
|
||||
AquariaLocationNames.OBJECTIVE_COMPLETE
|
||||
]
|
||||
items = [["Nature form"]]
|
||||
items = [[ItemNames.NATURE_FORM]]
|
||||
self.assertAccessDependency(locations, items)
|
||||
|
||||
@@ -6,6 +6,7 @@ Description: Unit test used to test that no progression items can be put in hard
|
||||
|
||||
from . import AquariaTestBase
|
||||
from BaseClasses import ItemClassification
|
||||
from ..Locations import AquariaLocationNames
|
||||
|
||||
|
||||
class UNoProgressionHardHiddenTest(AquariaTestBase):
|
||||
@@ -15,31 +16,31 @@ class UNoProgressionHardHiddenTest(AquariaTestBase):
|
||||
}
|
||||
|
||||
unfillable_locations = [
|
||||
"Energy Temple boss area, Fallen God Tooth",
|
||||
"Mithalas boss area, beating Mithalan God",
|
||||
"Kelp Forest boss area, beating Drunian God",
|
||||
"Sun Temple boss area, beating Sun God",
|
||||
"Sunken City, bulb on top of the boss area",
|
||||
"Home Water, Nautilus Egg",
|
||||
"Energy Temple blaster room, Blaster Egg",
|
||||
"Mithalas City Castle, beating the Priests",
|
||||
"Mermog cave, Piranha Egg",
|
||||
"Octopus Cave, Dumbo Egg",
|
||||
"King Jellyfish Cave, bulb in the right path from King Jelly",
|
||||
"King Jellyfish Cave, Jellyfish Costume",
|
||||
"Final Boss area, bulb in the boss third form room",
|
||||
"Sun Worm path, first cliff bulb",
|
||||
"Sun Worm path, second cliff bulb",
|
||||
"The Veil top right area, bulb at the top of the waterfall",
|
||||
"Bubble Cave, bulb in the left cave wall",
|
||||
"Bubble Cave, bulb in the right cave wall (behind the ice crystal)",
|
||||
"Bubble Cave, Verse Egg",
|
||||
"Kelp Forest bottom left area, bulb close to the spirit crystals",
|
||||
"Kelp Forest bottom left area, Walker Baby",
|
||||
"Sun Temple, Sun Key",
|
||||
"The Body bottom area, Mutant Costume",
|
||||
"Sun Temple, bulb in the hidden room of the right part",
|
||||
"Arnassi Ruins, Arnassi Armor",
|
||||
AquariaLocationNames.ENERGY_TEMPLE_BOSS_AREA_FALLEN_GOD_TOOTH,
|
||||
AquariaLocationNames.MITHALAS_BOSS_AREA_BEATING_MITHALAN_GOD,
|
||||
AquariaLocationNames.KELP_FOREST_BOSS_AREA_BEATING_DRUNIAN_GOD,
|
||||
AquariaLocationNames.SUN_TEMPLE_BOSS_AREA_BEATING_LUMEREAN_GOD,
|
||||
AquariaLocationNames.SUNKEN_CITY_BULB_ON_TOP_OF_THE_BOSS_AREA,
|
||||
AquariaLocationNames.HOME_WATERS_NAUTILUS_EGG,
|
||||
AquariaLocationNames.ENERGY_TEMPLE_BLASTER_ROOM_BLASTER_EGG,
|
||||
AquariaLocationNames.MITHALAS_CITY_CASTLE_BEATING_THE_PRIESTS,
|
||||
AquariaLocationNames.MERMOG_CAVE_PIRANHA_EGG,
|
||||
AquariaLocationNames.OCTOPUS_CAVE_DUMBO_EGG,
|
||||
AquariaLocationNames.KING_JELLYFISH_CAVE_BULB_IN_THE_RIGHT_PATH_FROM_KING_JELLY,
|
||||
AquariaLocationNames.KING_JELLYFISH_CAVE_JELLYFISH_COSTUME,
|
||||
AquariaLocationNames.FINAL_BOSS_AREA_BULB_IN_THE_BOSS_THIRD_FORM_ROOM,
|
||||
AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_FIRST_CLIFF_BULB,
|
||||
AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_SECOND_CLIFF_BULB,
|
||||
AquariaLocationNames.THE_VEIL_TOP_RIGHT_AREA_BULB_AT_THE_TOP_OF_THE_WATERFALL,
|
||||
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.KELP_FOREST_BOTTOM_LEFT_AREA_BULB_CLOSE_TO_THE_SPIRIT_CRYSTALS,
|
||||
AquariaLocationNames.KELP_FOREST_BOTTOM_LEFT_AREA_WALKER_BABY,
|
||||
AquariaLocationNames.SUN_TEMPLE_SUN_KEY,
|
||||
AquariaLocationNames.THE_BODY_BOTTOM_AREA_MUTANT_COSTUME,
|
||||
AquariaLocationNames.SUN_TEMPLE_BULB_IN_THE_HIDDEN_ROOM_OF_THE_RIGHT_PART,
|
||||
AquariaLocationNames.ARNASSI_RUINS_ARNASSI_ARMOR,
|
||||
]
|
||||
|
||||
def test_unconfine_home_water_both_location_fillable(self) -> None:
|
||||
|
||||
@@ -5,6 +5,7 @@ Description: Unit test used to test that progression items can be put in hard or
|
||||
"""
|
||||
|
||||
from . import AquariaTestBase
|
||||
from ..Locations import AquariaLocationNames
|
||||
|
||||
|
||||
class UNoProgressionHardHiddenTest(AquariaTestBase):
|
||||
@@ -14,31 +15,31 @@ class UNoProgressionHardHiddenTest(AquariaTestBase):
|
||||
}
|
||||
|
||||
unfillable_locations = [
|
||||
"Energy Temple boss area, Fallen God Tooth",
|
||||
"Mithalas boss area, beating Mithalan God",
|
||||
"Kelp Forest boss area, beating Drunian God",
|
||||
"Sun Temple boss area, beating Sun God",
|
||||
"Sunken City, bulb on top of the boss area",
|
||||
"Home Water, Nautilus Egg",
|
||||
"Energy Temple blaster room, Blaster Egg",
|
||||
"Mithalas City Castle, beating the Priests",
|
||||
"Mermog cave, Piranha Egg",
|
||||
"Octopus Cave, Dumbo Egg",
|
||||
"King Jellyfish Cave, bulb in the right path from King Jelly",
|
||||
"King Jellyfish Cave, Jellyfish Costume",
|
||||
"Final Boss area, bulb in the boss third form room",
|
||||
"Sun Worm path, first cliff bulb",
|
||||
"Sun Worm path, second cliff bulb",
|
||||
"The Veil top right area, bulb at the top of the waterfall",
|
||||
"Bubble Cave, bulb in the left cave wall",
|
||||
"Bubble Cave, bulb in the right cave wall (behind the ice crystal)",
|
||||
"Bubble Cave, Verse Egg",
|
||||
"Kelp Forest bottom left area, bulb close to the spirit crystals",
|
||||
"Kelp Forest bottom left area, Walker Baby",
|
||||
"Sun Temple, Sun Key",
|
||||
"The Body bottom area, Mutant Costume",
|
||||
"Sun Temple, bulb in the hidden room of the right part",
|
||||
"Arnassi Ruins, Arnassi Armor",
|
||||
AquariaLocationNames.ENERGY_TEMPLE_BOSS_AREA_FALLEN_GOD_TOOTH,
|
||||
AquariaLocationNames.MITHALAS_BOSS_AREA_BEATING_MITHALAN_GOD,
|
||||
AquariaLocationNames.KELP_FOREST_BOSS_AREA_BEATING_DRUNIAN_GOD,
|
||||
AquariaLocationNames.SUN_TEMPLE_BOSS_AREA_BEATING_LUMEREAN_GOD,
|
||||
AquariaLocationNames.SUNKEN_CITY_BULB_ON_TOP_OF_THE_BOSS_AREA,
|
||||
AquariaLocationNames.HOME_WATERS_NAUTILUS_EGG,
|
||||
AquariaLocationNames.ENERGY_TEMPLE_BLASTER_ROOM_BLASTER_EGG,
|
||||
AquariaLocationNames.MITHALAS_CITY_CASTLE_BEATING_THE_PRIESTS,
|
||||
AquariaLocationNames.MERMOG_CAVE_PIRANHA_EGG,
|
||||
AquariaLocationNames.OCTOPUS_CAVE_DUMBO_EGG,
|
||||
AquariaLocationNames.KING_JELLYFISH_CAVE_BULB_IN_THE_RIGHT_PATH_FROM_KING_JELLY,
|
||||
AquariaLocationNames.KING_JELLYFISH_CAVE_JELLYFISH_COSTUME,
|
||||
AquariaLocationNames.FINAL_BOSS_AREA_BULB_IN_THE_BOSS_THIRD_FORM_ROOM,
|
||||
AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_FIRST_CLIFF_BULB,
|
||||
AquariaLocationNames.SUN_TEMPLE_BOSS_PATH_SECOND_CLIFF_BULB,
|
||||
AquariaLocationNames.THE_VEIL_TOP_RIGHT_AREA_BULB_AT_THE_TOP_OF_THE_WATERFALL,
|
||||
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.KELP_FOREST_BOTTOM_LEFT_AREA_BULB_CLOSE_TO_THE_SPIRIT_CRYSTALS,
|
||||
AquariaLocationNames.KELP_FOREST_BOTTOM_LEFT_AREA_WALKER_BABY,
|
||||
AquariaLocationNames.SUN_TEMPLE_SUN_KEY,
|
||||
AquariaLocationNames.THE_BODY_BOTTOM_AREA_MUTANT_COSTUME,
|
||||
AquariaLocationNames.SUN_TEMPLE_BULB_IN_THE_HIDDEN_ROOM_OF_THE_RIGHT_PART,
|
||||
AquariaLocationNames.ARNASSI_RUINS_ARNASSI_ARMOR,
|
||||
]
|
||||
|
||||
def test_unconfine_home_water_both_location_fillable(self) -> None:
|
||||
|
||||
@@ -5,6 +5,8 @@ Description: Unit test used to test accessibility of locations with and without
|
||||
"""
|
||||
|
||||
from . import AquariaTestBase
|
||||
from ..Items import ItemNames
|
||||
from ..Locations import AquariaLocationNames
|
||||
|
||||
|
||||
class SpiritFormAccessTest(AquariaTestBase):
|
||||
@@ -13,23 +15,23 @@ class SpiritFormAccessTest(AquariaTestBase):
|
||||
def test_spirit_form_location(self) -> None:
|
||||
"""Test locations that require spirit form"""
|
||||
locations = [
|
||||
"The Veil bottom area, bulb in the spirit path",
|
||||
"Mithalas City Castle, Trident Head",
|
||||
"Open Water skeleton path, King Skull",
|
||||
"Kelp Forest bottom left area, Walker Baby",
|
||||
"Abyss right area, bulb behind the rock in the whale room",
|
||||
"The Whale, Verse Egg",
|
||||
"Ice Cave, bulb in the room to the right",
|
||||
"Ice Cave, first bulb in the top exit room",
|
||||
"Ice Cave, second bulb in the top exit room",
|
||||
"Ice Cave, third bulb in the top exit room",
|
||||
"Ice Cave, bulb in the left room",
|
||||
"Bubble Cave, bulb in the left cave wall",
|
||||
"Bubble Cave, bulb in the right cave wall (behind the ice crystal)",
|
||||
"Bubble Cave, Verse Egg",
|
||||
"Sunken City left area, Girl Costume",
|
||||
"Beating Mantis Shrimp Prime",
|
||||
"First secret",
|
||||
AquariaLocationNames.THE_VEIL_BOTTOM_AREA_BULB_IN_THE_SPIRIT_PATH,
|
||||
AquariaLocationNames.MITHALAS_CITY_CASTLE_TRIDENT_HEAD,
|
||||
AquariaLocationNames.OPEN_WATERS_SKELETON_PATH_KING_SKULL,
|
||||
AquariaLocationNames.KELP_FOREST_BOTTOM_LEFT_AREA_WALKER_BABY,
|
||||
AquariaLocationNames.ABYSS_RIGHT_AREA_BULB_BEHIND_THE_ROCK_IN_THE_WHALE_ROOM,
|
||||
AquariaLocationNames.THE_WHALE_VERSE_EGG,
|
||||
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,
|
||||
AquariaLocationNames.SUNKEN_CITY_LEFT_AREA_GIRL_COSTUME,
|
||||
AquariaLocationNames.BEATING_MANTIS_SHRIMP_PRIME,
|
||||
AquariaLocationNames.FIRST_SECRET,
|
||||
]
|
||||
items = [["Spirit form"]]
|
||||
items = [[ItemNames.SPIRIT_FORM]]
|
||||
self.assertAccessDependency(locations, items)
|
||||
|
||||
@@ -5,6 +5,8 @@ Description: Unit test used to test accessibility of locations with and without
|
||||
"""
|
||||
|
||||
from . import AquariaTestBase
|
||||
from ..Items import ItemNames
|
||||
from ..Locations import AquariaLocationNames
|
||||
|
||||
|
||||
class SunFormAccessTest(AquariaTestBase):
|
||||
@@ -13,16 +15,16 @@ class SunFormAccessTest(AquariaTestBase):
|
||||
def test_sun_form_location(self) -> None:
|
||||
"""Test locations that require sun form"""
|
||||
locations = [
|
||||
"First secret",
|
||||
"The Whale, Verse Egg",
|
||||
"Abyss right area, bulb behind the rock in the whale room",
|
||||
"Octopus Cave, Dumbo Egg",
|
||||
"Beating Octopus Prime",
|
||||
"Sunken City, bulb on top of the boss area",
|
||||
"Beating the Golem",
|
||||
"Sunken City cleared",
|
||||
"Final Boss area, bulb in the boss third form room",
|
||||
"Objective complete"
|
||||
AquariaLocationNames.FIRST_SECRET,
|
||||
AquariaLocationNames.THE_WHALE_VERSE_EGG,
|
||||
AquariaLocationNames.ABYSS_RIGHT_AREA_BULB_BEHIND_THE_ROCK_IN_THE_WHALE_ROOM,
|
||||
AquariaLocationNames.OCTOPUS_CAVE_DUMBO_EGG,
|
||||
AquariaLocationNames.BEATING_OCTOPUS_PRIME,
|
||||
AquariaLocationNames.SUNKEN_CITY_BULB_ON_TOP_OF_THE_BOSS_AREA,
|
||||
AquariaLocationNames.BEATING_THE_GOLEM,
|
||||
AquariaLocationNames.SUNKEN_CITY_CLEARED,
|
||||
AquariaLocationNames.FINAL_BOSS_AREA_BULB_IN_THE_BOSS_THIRD_FORM_ROOM,
|
||||
AquariaLocationNames.OBJECTIVE_COMPLETE
|
||||
]
|
||||
items = [["Sun form"]]
|
||||
items = [[ItemNames.SUN_FORM]]
|
||||
self.assertAccessDependency(locations, items)
|
||||
|
||||
@@ -6,16 +6,17 @@ Description: Unit test used to test accessibility of region with the unconfined
|
||||
"""
|
||||
|
||||
from . import AquariaTestBase
|
||||
from ..Options import UnconfineHomeWater, EarlyEnergyForm
|
||||
|
||||
|
||||
class UnconfineHomeWaterBothAccessTest(AquariaTestBase):
|
||||
"""Unit test used to test accessibility of region with the unconfine home water option enabled"""
|
||||
options = {
|
||||
"unconfine_home_water": 3,
|
||||
"early_energy_form": False
|
||||
"unconfine_home_water": UnconfineHomeWater.option_via_both,
|
||||
"early_energy_form": EarlyEnergyForm.option_off
|
||||
}
|
||||
|
||||
def test_unconfine_home_water_both_location(self) -> None:
|
||||
"""Test locations accessible with unconfined home water via energy door and transportation turtle"""
|
||||
self.assertTrue(self.can_reach_region("Open Water top left area"), "Cannot reach Open Water top left area")
|
||||
self.assertTrue(self.can_reach_region("Home Water, turtle room"), "Cannot reach Home Water, turtle room")
|
||||
self.assertTrue(self.can_reach_region("Open Waters top left area"), "Cannot reach Open Waters top left area")
|
||||
self.assertTrue(self.can_reach_region("Home Waters, turtle room"), "Cannot reach Home Waters, turtle room")
|
||||
|
||||
@@ -5,16 +5,17 @@ Description: Unit test used to test accessibility of region with the unconfined
|
||||
"""
|
||||
|
||||
from . import AquariaTestBase
|
||||
from ..Options import UnconfineHomeWater, EarlyEnergyForm
|
||||
|
||||
|
||||
class UnconfineHomeWaterEnergyDoorAccessTest(AquariaTestBase):
|
||||
"""Unit test used to test accessibility of region with the unconfine home water option enabled"""
|
||||
options = {
|
||||
"unconfine_home_water": 1,
|
||||
"early_energy_form": False
|
||||
"unconfine_home_water": UnconfineHomeWater.option_via_energy_door,
|
||||
"early_energy_form": EarlyEnergyForm.option_off
|
||||
}
|
||||
|
||||
def test_unconfine_home_water_energy_door_location(self) -> None:
|
||||
"""Test locations accessible with unconfined home water via energy door"""
|
||||
self.assertTrue(self.can_reach_region("Open Water top left area"), "Cannot reach Open Water top left area")
|
||||
self.assertFalse(self.can_reach_region("Home Water, turtle room"), "Can reach Home Water, turtle room")
|
||||
self.assertTrue(self.can_reach_region("Open Waters top left area"), "Cannot reach Open Waters top left area")
|
||||
self.assertFalse(self.can_reach_region("Home Waters, turtle room"), "Can reach Home Waters, turtle room")
|
||||
|
||||
@@ -5,16 +5,17 @@ Description: Unit test used to test accessibility of region with the unconfined
|
||||
"""
|
||||
|
||||
from . import AquariaTestBase
|
||||
from ..Options import UnconfineHomeWater, EarlyEnergyForm
|
||||
|
||||
|
||||
class UnconfineHomeWaterTransturtleAccessTest(AquariaTestBase):
|
||||
"""Unit test used to test accessibility of region with the unconfine home water option enabled"""
|
||||
options = {
|
||||
"unconfine_home_water": 2,
|
||||
"early_energy_form": False
|
||||
"unconfine_home_water": UnconfineHomeWater.option_via_transturtle,
|
||||
"early_energy_form": EarlyEnergyForm.option_off
|
||||
}
|
||||
|
||||
def test_unconfine_home_water_transturtle_location(self) -> None:
|
||||
"""Test locations accessible with unconfined home water via transportation turtle"""
|
||||
self.assertTrue(self.can_reach_region("Home Water, turtle room"), "Cannot reach Home Water, turtle room")
|
||||
self.assertFalse(self.can_reach_region("Open Water top left area"), "Can reach Open Water top left area")
|
||||
self.assertTrue(self.can_reach_region("Home Waters, turtle room"), "Cannot reach Home Waters, turtle room")
|
||||
self.assertFalse(self.can_reach_region("Open Waters top left area"), "Can reach Open Waters top left area")
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from dataclasses import dataclass
|
||||
from Options import Choice, Toggle, DefaultOnToggle, DeathLink, PerGameCommonOptions, OptionGroup
|
||||
from Options import Choice, Toggle, DefaultOnToggle, DeathLink, PerGameCommonOptions, StartInventoryPool, OptionGroup
|
||||
import random
|
||||
|
||||
|
||||
@@ -213,6 +213,7 @@ class BlasphemousDeathLink(DeathLink):
|
||||
|
||||
@dataclass
|
||||
class BlasphemousOptions(PerGameCommonOptions):
|
||||
start_inventory_from_pool: StartInventoryPool
|
||||
prie_dieu_warp: PrieDieuWarp
|
||||
skip_cutscenes: SkipCutscenes
|
||||
corpse_hints: CorpseHints
|
||||
|
||||
@@ -137,12 +137,6 @@ class BlasphemousWorld(World):
|
||||
]
|
||||
|
||||
skipped_items = []
|
||||
junk: int = 0
|
||||
|
||||
for item, count in self.options.start_inventory.value.items():
|
||||
for _ in range(count):
|
||||
skipped_items.append(item)
|
||||
junk += 1
|
||||
|
||||
skipped_items.extend(unrandomized_dict.values())
|
||||
|
||||
@@ -194,9 +188,6 @@ class BlasphemousWorld(World):
|
||||
for _ in range(count):
|
||||
pool.append(self.create_item(item["name"]))
|
||||
|
||||
for _ in range(junk):
|
||||
pool.append(self.create_item(self.get_filler_item_name()))
|
||||
|
||||
self.multiworld.itempool += pool
|
||||
|
||||
self.place_items_from_dict(unrandomized_dict)
|
||||
|
||||
248
worlds/cvcotm/LICENSES.txt
Normal file
248
worlds/cvcotm/LICENSES.txt
Normal file
@@ -0,0 +1,248 @@
|
||||
|
||||
Regarding the sprite data specifically for the Archipelago logo found in data > patches.py:
|
||||
|
||||
The Archipelago Logo is © 2022 by Krista Corkos and Christopher Wilson and licensed under Attribution-NonCommercial 4.0
|
||||
International. Logo modified by Liquid Cat to fit artstyle and uses within this mod. To view a copy of this license,
|
||||
visit http://creativecommons.org/licenses/by-nc/4.0/
|
||||
|
||||
The other custom sprites that I made, as long as you don't lie by claiming you were the one who drew them, I am fine
|
||||
with you using and distributing them however you want to. -Liquid Cat
|
||||
|
||||
========================================================================================================================
|
||||
|
||||
For the lz10.py and cvcotm_text.py modules specifically the MIT license applies. Its terms are as follows:
|
||||
|
||||
MIT License
|
||||
|
||||
cvcotm_text.py Copyright (c) 2024 Liquid Cat
|
||||
(Please consider the associated pixel data for the ASCII characters missing from CotM in data > patches.py
|
||||
in the public domain, if there was any thought that that could even be copyrighted. -Liquid Cat)
|
||||
|
||||
lz10.py Copyright (c) 2024 lilDavid, NoiseCrush
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
|
||||
========================================================================================================================
|
||||
|
||||
Everything else in this world package not mentioned above can be assumed covered by standalone CotMR's Apache license
|
||||
being a piece of a direct derivative of it. The terms are as follows:
|
||||
|
||||
Apache License
|
||||
Version 2.0, January 2004
|
||||
http://www.apache.org/licenses/
|
||||
|
||||
TERMS AND CONDITIONS FOR USE, REPRODUCTION, AND DISTRIBUTION
|
||||
|
||||
1. Definitions.
|
||||
|
||||
"License" shall mean the terms and conditions for use, reproduction,
|
||||
and distribution as defined by Sections 1 through 9 of this document.
|
||||
|
||||
"Licensor" shall mean the copyright owner or entity authorized by
|
||||
the copyright owner that is granting the License.
|
||||
|
||||
"Legal Entity" shall mean the union of the acting entity and all
|
||||
other entities that control, are controlled by, or are under common
|
||||
control with that entity. For the purposes of this definition,
|
||||
"control" means (i) the power, direct or indirect, to cause the
|
||||
direction or management of such entity, whether by contract or
|
||||
otherwise, or (ii) ownership of fifty percent (50%) or more of the
|
||||
outstanding shares, or (iii) beneficial ownership of such entity.
|
||||
|
||||
"You" (or "Your") shall mean an individual or Legal Entity
|
||||
exercising permissions granted by this License.
|
||||
|
||||
"Source" form shall mean the preferred form for making modifications,
|
||||
including but not limited to software source code, documentation
|
||||
source, and configuration files.
|
||||
|
||||
"Object" form shall mean any form resulting from mechanical
|
||||
transformation or translation of a Source form, including but
|
||||
not limited to compiled object code, generated documentation,
|
||||
and conversions to other media types.
|
||||
|
||||
"Work" shall mean the work of authorship, whether in Source or
|
||||
Object form, made available under the License, as indicated by a
|
||||
copyright notice that is included in or attached to the work
|
||||
(an example is provided in the Appendix below).
|
||||
|
||||
"Derivative Works" shall mean any work, whether in Source or Object
|
||||
form, that is based on (or derived from) the Work and for which the
|
||||
editorial revisions, annotations, elaborations, or other modifications
|
||||
represent, as a whole, an original work of authorship. For the purposes
|
||||
of this License, Derivative Works shall not include works that remain
|
||||
separable from, or merely link (or bind by name) to the interfaces of,
|
||||
the Work and Derivative Works thereof.
|
||||
|
||||
"Contribution" shall mean any work of authorship, including
|
||||
the original version of the Work and any modifications or additions
|
||||
to that Work or Derivative Works thereof, that is intentionally
|
||||
submitted to Licensor for inclusion in the Work by the copyright owner
|
||||
or by an individual or Legal Entity authorized to submit on behalf of
|
||||
the copyright owner. For the purposes of this definition, "submitted"
|
||||
means any form of electronic, verbal, or written communication sent
|
||||
to the Licensor or its representatives, including but not limited to
|
||||
communication on electronic mailing lists, source code control systems,
|
||||
and issue tracking systems that are managed by, or on behalf of, the
|
||||
Licensor for the purpose of discussing and improving the Work, but
|
||||
excluding communication that is conspicuously marked or otherwise
|
||||
designated in writing by the copyright owner as "Not a Contribution."
|
||||
|
||||
"Contributor" shall mean Licensor and any individual or Legal Entity
|
||||
on behalf of whom a Contribution has been received by Licensor and
|
||||
subsequently incorporated within the Work.
|
||||
|
||||
2. Grant of Copyright License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
copyright license to reproduce, prepare Derivative Works of,
|
||||
publicly display, publicly perform, sublicense, and distribute the
|
||||
Work and such Derivative Works in Source or Object form.
|
||||
|
||||
3. Grant of Patent License. Subject to the terms and conditions of
|
||||
this License, each Contributor hereby grants to You a perpetual,
|
||||
worldwide, non-exclusive, no-charge, royalty-free, irrevocable
|
||||
(except as stated in this section) patent license to make, have made,
|
||||
use, offer to sell, sell, import, and otherwise transfer the Work,
|
||||
where such license applies only to those patent claims licensable
|
||||
by such Contributor that are necessarily infringed by their
|
||||
Contribution(s) alone or by combination of their Contribution(s)
|
||||
with the Work to which such Contribution(s) was submitted. If You
|
||||
institute patent litigation against any entity (including a
|
||||
cross-claim or counterclaim in a lawsuit) alleging that the Work
|
||||
or a Contribution incorporated within the Work constitutes direct
|
||||
or contributory patent infringement, then any patent licenses
|
||||
granted to You under this License for that Work shall terminate
|
||||
as of the date such litigation is filed.
|
||||
|
||||
4. Redistribution. You may reproduce and distribute copies of the
|
||||
Work or Derivative Works thereof in any medium, with or without
|
||||
modifications, and in Source or Object form, provided that You
|
||||
meet the following conditions:
|
||||
|
||||
(a) You must give any other recipients of the Work or
|
||||
Derivative Works a copy of this License; and
|
||||
|
||||
(b) You must cause any modified files to carry prominent notices
|
||||
stating that You changed the files; and
|
||||
|
||||
(c) You must retain, in the Source form of any Derivative Works
|
||||
that You distribute, all copyright, patent, trademark, and
|
||||
attribution notices from the Source form of the Work,
|
||||
excluding those notices that do not pertain to any part of
|
||||
the Derivative Works; and
|
||||
|
||||
(d) If the Work includes a "NOTICE" text file as part of its
|
||||
distribution, then any Derivative Works that You distribute must
|
||||
include a readable copy of the attribution notices contained
|
||||
within such NOTICE file, excluding those notices that do not
|
||||
pertain to any part of the Derivative Works, in at least one
|
||||
of the following places: within a NOTICE text file distributed
|
||||
as part of the Derivative Works; within the Source form or
|
||||
documentation, if provided along with the Derivative Works; or,
|
||||
within a display generated by the Derivative Works, if and
|
||||
wherever such third-party notices normally appear. The contents
|
||||
of the NOTICE file are for informational purposes only and
|
||||
do not modify the License. You may add Your own attribution
|
||||
notices within Derivative Works that You distribute, alongside
|
||||
or as an addendum to the NOTICE text from the Work, provided
|
||||
that such additional attribution notices cannot be construed
|
||||
as modifying the License.
|
||||
|
||||
You may add Your own copyright statement to Your modifications and
|
||||
may provide additional or different license terms and conditions
|
||||
for use, reproduction, or distribution of Your modifications, or
|
||||
for any such Derivative Works as a whole, provided Your use,
|
||||
reproduction, and distribution of the Work otherwise complies with
|
||||
the conditions stated in this License.
|
||||
|
||||
5. Submission of Contributions. Unless You explicitly state otherwise,
|
||||
any Contribution intentionally submitted for inclusion in the Work
|
||||
by You to the Licensor shall be under the terms and conditions of
|
||||
this License, without any additional terms or conditions.
|
||||
Notwithstanding the above, nothing herein shall supersede or modify
|
||||
the terms of any separate license agreement you may have executed
|
||||
with Licensor regarding such Contributions.
|
||||
|
||||
6. Trademarks. This License does not grant permission to use the trade
|
||||
names, trademarks, service marks, or product names of the Licensor,
|
||||
except as required for reasonable and customary use in describing the
|
||||
origin of the Work and reproducing the content of the NOTICE file.
|
||||
|
||||
7. Disclaimer of Warranty. Unless required by applicable law or
|
||||
agreed to in writing, Licensor provides the Work (and each
|
||||
Contributor provides its Contributions) on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or
|
||||
implied, including, without limitation, any warranties or conditions
|
||||
of TITLE, NON-INFRINGEMENT, MERCHANTABILITY, or FITNESS FOR A
|
||||
PARTICULAR PURPOSE. You are solely responsible for determining the
|
||||
appropriateness of using or redistributing the Work and assume any
|
||||
risks associated with Your exercise of permissions under this License.
|
||||
|
||||
8. Limitation of Liability. In no event and under no legal theory,
|
||||
whether in tort (including negligence), contract, or otherwise,
|
||||
unless required by applicable law (such as deliberate and grossly
|
||||
negligent acts) or agreed to in writing, shall any Contributor be
|
||||
liable to You for damages, including any direct, indirect, special,
|
||||
incidental, or consequential damages of any character arising as a
|
||||
result of this License or out of the use or inability to use the
|
||||
Work (including but not limited to damages for loss of goodwill,
|
||||
work stoppage, computer failure or malfunction, or any and all
|
||||
other commercial damages or losses), even if such Contributor
|
||||
has been advised of the possibility of such damages.
|
||||
|
||||
9. Accepting Warranty or Additional Liability. While redistributing
|
||||
the Work or Derivative Works thereof, You may choose to offer,
|
||||
and charge a fee for, acceptance of support, warranty, indemnity,
|
||||
or other liability obligations and/or rights consistent with this
|
||||
License. However, in accepting such obligations, You may act only
|
||||
on Your own behalf and on Your sole responsibility, not on behalf
|
||||
of any other Contributor, and only if You agree to indemnify,
|
||||
defend, and hold each Contributor harmless for any liability
|
||||
incurred by, or claims asserted against, such Contributor by reason
|
||||
of your accepting any such warranty or additional liability.
|
||||
|
||||
END OF TERMS AND CONDITIONS
|
||||
|
||||
APPENDIX: How to apply the Apache License to your work.
|
||||
|
||||
To apply the Apache License to your work, attach the following
|
||||
boilerplate notice, with the fields enclosed by brackets "[]"
|
||||
replaced with your own identifying information. (Don't include
|
||||
the brackets!) The text should be enclosed in the appropriate
|
||||
comment syntax for the file format. We also recommend that a
|
||||
file or class name and description of purpose be included on the
|
||||
same "printed page" as the copyright notice for easier
|
||||
identification within third-party archives.
|
||||
|
||||
Copyright 2021-2024 DevAnj, fusecavator, spooky, Malaert64
|
||||
|
||||
Archipelago version by Liquid Cat
|
||||
|
||||
Licensed under the Apache License, Version 2.0 (the "License");
|
||||
you may not use this file except in compliance with the License.
|
||||
You may obtain a copy of the License at
|
||||
|
||||
http://www.apache.org/licenses/LICENSE-2.0
|
||||
|
||||
Unless required by applicable law or agreed to in writing, software
|
||||
distributed under the License is distributed on an "AS IS" BASIS,
|
||||
WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
See the License for the specific language governing permissions and
|
||||
limitations under the License.
|
||||
4
worlds/cvcotm/NOTICE.txt
Normal file
4
worlds/cvcotm/NOTICE.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
Circle of the Moon Randomizer
|
||||
Copyright 2021-2024 DevAnj, fusecavator, spooky, Malaert64
|
||||
|
||||
Archipelago version by Liquid Cat
|
||||
221
worlds/cvcotm/__init__.py
Normal file
221
worlds/cvcotm/__init__.py
Normal file
@@ -0,0 +1,221 @@
|
||||
import os
|
||||
import typing
|
||||
import settings
|
||||
import base64
|
||||
import logging
|
||||
|
||||
from BaseClasses import Item, Region, Tutorial, ItemClassification
|
||||
from .items import CVCotMItem, FILLER_ITEM_NAMES, ACTION_CARDS, ATTRIBUTE_CARDS, cvcotm_item_info, \
|
||||
get_item_names_to_ids, get_item_counts
|
||||
from .locations import CVCotMLocation, get_location_names_to_ids, BASE_ID, get_named_locations_data, \
|
||||
get_location_name_groups
|
||||
from .options import cvcotm_option_groups, CVCotMOptions, SubWeaponShuffle, IronMaidenBehavior, RequiredSkirmishes, \
|
||||
CompletionGoal, EarlyEscapeItem
|
||||
from .regions import get_region_info, get_all_region_names
|
||||
from .rules import CVCotMRules
|
||||
from .data import iname, lname
|
||||
from .presets import cvcotm_options_presets
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
|
||||
from .aesthetics import shuffle_sub_weapons, get_location_data, get_countdown_flags, populate_enemy_drops, \
|
||||
get_start_inventory_data
|
||||
from .rom import RomData, patch_rom, get_base_rom_path, CVCotMProcedurePatch, CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH, \
|
||||
CVCOTM_VC_US_HASH
|
||||
from .client import CastlevaniaCotMClient
|
||||
|
||||
|
||||
class CVCotMSettings(settings.Group):
|
||||
class RomFile(settings.UserFilePath):
|
||||
"""File name of the Castlevania CotM US rom"""
|
||||
copy_to = "Castlevania - Circle of the Moon (USA).gba"
|
||||
description = "Castlevania CotM (US) ROM File"
|
||||
md5s = [CVCOTM_CT_US_HASH, CVCOTM_AC_US_HASH, CVCOTM_VC_US_HASH]
|
||||
|
||||
rom_file: RomFile = RomFile(RomFile.copy_to)
|
||||
|
||||
|
||||
class CVCotMWeb(WebWorld):
|
||||
theme = "stone"
|
||||
options_presets = cvcotm_options_presets
|
||||
|
||||
tutorials = [Tutorial(
|
||||
"Multiworld Setup Guide",
|
||||
"A guide to setting up the Archipleago Castlevania: Circle of the Moon randomizer on your computer and "
|
||||
"connecting it to a multiworld.",
|
||||
"English",
|
||||
"setup_en.md",
|
||||
"setup/en",
|
||||
["Liquid Cat"]
|
||||
)]
|
||||
|
||||
option_groups = cvcotm_option_groups
|
||||
|
||||
|
||||
class CVCotMWorld(World):
|
||||
"""
|
||||
Castlevania: Circle of the Moon is a launch title for the Game Boy Advance and the first of three Castlevania games
|
||||
released for the handheld in the "Metroidvania" format. As Nathan Graves, wielding the Hunter Whip and utilizing the
|
||||
Dual Set-Up System for new possibilities, you must battle your way through Camilla's castle and rescue your master
|
||||
from a demonic ritual to restore the Count's power...
|
||||
"""
|
||||
game = "Castlevania - Circle of the Moon"
|
||||
item_name_groups = {
|
||||
"DSS": ACTION_CARDS.union(ATTRIBUTE_CARDS),
|
||||
"Card": ACTION_CARDS.union(ATTRIBUTE_CARDS),
|
||||
"Action": ACTION_CARDS,
|
||||
"Action Card": ACTION_CARDS,
|
||||
"Attribute": ATTRIBUTE_CARDS,
|
||||
"Attribute Card": ATTRIBUTE_CARDS,
|
||||
"Freeze": {iname.serpent, iname.cockatrice, iname.mercury, iname.mars},
|
||||
"Freeze Action": {iname.mercury, iname.mars},
|
||||
"Freeze Attribute": {iname.serpent, iname.cockatrice}
|
||||
}
|
||||
location_name_groups = get_location_name_groups()
|
||||
options_dataclass = CVCotMOptions
|
||||
options: CVCotMOptions
|
||||
settings: typing.ClassVar[CVCotMSettings]
|
||||
origin_region_name = "Catacomb"
|
||||
hint_blacklist = frozenset({lname.ba24}) # The Battle Arena reward, if it's put in, will always be a Last Key.
|
||||
|
||||
item_name_to_id = {name: cvcotm_item_info[name].code + BASE_ID for name in cvcotm_item_info
|
||||
if cvcotm_item_info[name].code is not None}
|
||||
location_name_to_id = get_location_names_to_ids()
|
||||
|
||||
# Default values to possibly be updated in generate_early
|
||||
total_last_keys: int = 0
|
||||
required_last_keys: int = 0
|
||||
|
||||
auth: bytearray
|
||||
|
||||
web = CVCotMWeb()
|
||||
|
||||
def generate_early(self) -> None:
|
||||
# Generate the player's unique authentication
|
||||
self.auth = bytearray(self.random.getrandbits(8) for _ in range(16))
|
||||
|
||||
# If Required Skirmishes are on, force the Required and Available Last Keys to 8 or 9 depending on which option
|
||||
# was chosen.
|
||||
if self.options.required_skirmishes == RequiredSkirmishes.option_all_bosses:
|
||||
self.options.required_last_keys.value = 8
|
||||
self.options.available_last_keys.value = 8
|
||||
elif self.options.required_skirmishes == RequiredSkirmishes.option_all_bosses_and_arena:
|
||||
self.options.required_last_keys.value = 9
|
||||
self.options.available_last_keys.value = 9
|
||||
|
||||
self.total_last_keys = self.options.available_last_keys.value
|
||||
self.required_last_keys = self.options.required_last_keys.value
|
||||
|
||||
# If there are more Last Keys required than there are Last Keys in total, drop the required Last Keys to
|
||||
# the total Last Keys.
|
||||
if self.required_last_keys > self.total_last_keys:
|
||||
self.required_last_keys = self.total_last_keys
|
||||
logging.warning(f"[{self.player_name}] The Required Last Keys "
|
||||
f"({self.options.required_last_keys.value}) is higher than the Available Last Keys "
|
||||
f"({self.options.available_last_keys.value}). Lowering the required number to: "
|
||||
f"{self.required_last_keys}")
|
||||
self.options.required_last_keys.value = self.required_last_keys
|
||||
|
||||
# Place the Double or Roc Wing in local_early_items if the Early Escape option is being used.
|
||||
if self.options.early_escape_item == EarlyEscapeItem.option_double:
|
||||
self.multiworld.local_early_items[self.player][iname.double] = 1
|
||||
elif self.options.early_escape_item == EarlyEscapeItem.option_roc_wing:
|
||||
self.multiworld.local_early_items[self.player][iname.roc_wing] = 1
|
||||
elif self.options.early_escape_item == EarlyEscapeItem.option_double_or_roc_wing:
|
||||
self.multiworld.local_early_items[self.player][self.random.choice([iname.double, iname.roc_wing])] = 1
|
||||
|
||||
def create_regions(self) -> None:
|
||||
# Create every Region object.
|
||||
created_regions = [Region(name, self.player, self.multiworld) for name in get_all_region_names()]
|
||||
|
||||
# Attach the Regions to the Multiworld.
|
||||
self.multiworld.regions.extend(created_regions)
|
||||
|
||||
for reg in created_regions:
|
||||
|
||||
# Add the Entrances to all the Regions.
|
||||
ent_destinations_and_names = get_region_info(reg.name, "entrances")
|
||||
if ent_destinations_and_names is not None:
|
||||
reg.add_exits(ent_destinations_and_names)
|
||||
|
||||
# Add the Locations to all the Regions.
|
||||
loc_names = get_region_info(reg.name, "locations")
|
||||
if loc_names is None:
|
||||
continue
|
||||
locations_with_ids, locked_pairs = get_named_locations_data(loc_names, self.options)
|
||||
reg.add_locations(locations_with_ids, CVCotMLocation)
|
||||
|
||||
# Place locked Items on all of their associated Locations.
|
||||
for locked_loc, locked_item in locked_pairs.items():
|
||||
self.get_location(locked_loc).place_locked_item(self.create_item(locked_item,
|
||||
ItemClassification.progression))
|
||||
|
||||
def create_item(self, name: str, force_classification: typing.Optional[ItemClassification] = None) -> Item:
|
||||
if force_classification is not None:
|
||||
classification = force_classification
|
||||
else:
|
||||
classification = cvcotm_item_info[name].default_classification
|
||||
|
||||
code = cvcotm_item_info[name].code
|
||||
if code is not None:
|
||||
code += BASE_ID
|
||||
|
||||
created_item = CVCotMItem(name, classification, code, self.player)
|
||||
|
||||
return created_item
|
||||
|
||||
def create_items(self) -> None:
|
||||
item_counts = get_item_counts(self)
|
||||
|
||||
# Set up the items correctly
|
||||
self.multiworld.itempool += [self.create_item(item, classification) for classification in item_counts for item
|
||||
in item_counts[classification] for _ in range(item_counts[classification][item])]
|
||||
|
||||
def set_rules(self) -> None:
|
||||
# Set all the Entrance and Location rules properly.
|
||||
CVCotMRules(self).set_cvcotm_rules()
|
||||
|
||||
def generate_output(self, output_directory: str) -> None:
|
||||
# Get out all the Locations that are not Events. Only take the Iron Maiden switch if the Maiden Detonator is in
|
||||
# the item pool.
|
||||
active_locations = [loc for loc in self.multiworld.get_locations(self.player) if loc.address is not None and
|
||||
(loc.name != lname.ct21 or self.options.iron_maiden_behavior ==
|
||||
IronMaidenBehavior.option_detonator_in_pool)]
|
||||
|
||||
# Location data
|
||||
offset_data = get_location_data(self, active_locations)
|
||||
# Sub-weapons
|
||||
if self.options.sub_weapon_shuffle:
|
||||
offset_data.update(shuffle_sub_weapons(self))
|
||||
# Item drop randomization
|
||||
if self.options.item_drop_randomization:
|
||||
offset_data.update(populate_enemy_drops(self))
|
||||
# Countdown
|
||||
if self.options.countdown:
|
||||
offset_data.update(get_countdown_flags(self, active_locations))
|
||||
# Start Inventory
|
||||
start_inventory_data = get_start_inventory_data(self)
|
||||
offset_data.update(start_inventory_data[0])
|
||||
|
||||
patch = CVCotMProcedurePatch(player=self.player, player_name=self.player_name)
|
||||
patch_rom(self, patch, offset_data, start_inventory_data[1])
|
||||
|
||||
rom_path = os.path.join(output_directory, f"{self.multiworld.get_out_file_name_base(self.player)}"
|
||||
f"{patch.patch_file_ending}")
|
||||
|
||||
patch.write(rom_path)
|
||||
|
||||
def fill_slot_data(self) -> dict:
|
||||
return {"death_link": self.options.death_link.value,
|
||||
"iron_maiden_behavior": self.options.iron_maiden_behavior.value,
|
||||
"ignore_cleansing": self.options.ignore_cleansing.value,
|
||||
"skip_tutorials": self.options.skip_tutorials.value,
|
||||
"required_last_keys": self.required_last_keys,
|
||||
"completion_goal": self.options.completion_goal.value}
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
return self.random.choice(FILLER_ITEM_NAMES)
|
||||
|
||||
def modify_multidata(self, multidata: typing.Dict[str, typing.Any]):
|
||||
# Put the player's unique authentication in connect_names.
|
||||
multidata["connect_names"][base64.b64encode(self.auth).decode("ascii")] = \
|
||||
multidata["connect_names"][self.player_name]
|
||||
761
worlds/cvcotm/aesthetics.py
Normal file
761
worlds/cvcotm/aesthetics.py
Normal file
@@ -0,0 +1,761 @@
|
||||
from BaseClasses import ItemClassification, Location
|
||||
from .options import ItemDropRandomization, Countdown, RequiredSkirmishes, IronMaidenBehavior
|
||||
from .locations import cvcotm_location_info
|
||||
from .items import cvcotm_item_info, MAJORS_CLASSIFICATIONS
|
||||
from .data import iname
|
||||
|
||||
from typing import TYPE_CHECKING, Dict, List, Iterable, Tuple, NamedTuple, Optional, TypedDict
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import CVCotMWorld
|
||||
|
||||
|
||||
class StatInfo(TypedDict):
|
||||
# Amount this stat increases per Max Up the player starts with.
|
||||
amount_per: int
|
||||
# The most amount of this stat the player is allowed to start with. Problems arise if the stat exceeds 9999, so we
|
||||
# must ensure it can't if the player raises any class to level 99 as well as collects 255 of that max up. The game
|
||||
# caps hearts at 999 automatically, so it doesn't matter so much for that one.
|
||||
max_allowed: int
|
||||
# The key variable in extra_stats that the stat max up affects.
|
||||
variable: str
|
||||
|
||||
|
||||
extra_starting_stat_info: Dict[str, StatInfo] = {
|
||||
iname.hp_max: {"amount_per": 10,
|
||||
"max_allowed": 5289,
|
||||
"variable": "extra health"},
|
||||
iname.mp_max: {"amount_per": 10,
|
||||
"max_allowed": 3129,
|
||||
"variable": "extra magic"},
|
||||
iname.heart_max: {"amount_per": 6,
|
||||
"max_allowed": 999,
|
||||
"variable": "extra hearts"},
|
||||
}
|
||||
|
||||
other_player_subtype_bytes = {
|
||||
0xE4: 0x03,
|
||||
0xE6: 0x14,
|
||||
0xE8: 0x0A
|
||||
}
|
||||
|
||||
|
||||
class OtherGameAppearancesInfo(TypedDict):
|
||||
# What type of item to place for the other player.
|
||||
type: int
|
||||
# What item to display it as for the other player.
|
||||
appearance: int
|
||||
|
||||
|
||||
other_game_item_appearances: Dict[str, Dict[str, OtherGameAppearancesInfo]] = {
|
||||
# NOTE: Symphony of the Night is currently an unsupported world not in main.
|
||||
"Symphony of the Night": {"Life Vessel": {"type": 0xE4,
|
||||
"appearance": 0x01},
|
||||
"Heart Vessel": {"type": 0xE4,
|
||||
"appearance": 0x00}},
|
||||
"Timespinner": {"Max HP": {"type": 0xE4,
|
||||
"appearance": 0x01},
|
||||
"Max Aura": {"type": 0xE4,
|
||||
"appearance": 0x02},
|
||||
"Max Sand": {"type": 0xE8,
|
||||
"appearance": 0x0F}}
|
||||
}
|
||||
|
||||
# 0 = Holy water 22
|
||||
# 1 = Axe 24
|
||||
# 2 = Knife 32
|
||||
# 3 = Cross 6
|
||||
# 4 = Stopwatch 12
|
||||
# 5 = Small heart
|
||||
# 6 = Big heart
|
||||
rom_sub_weapon_offsets = {
|
||||
0xD034E: b"\x01",
|
||||
0xD0462: b"\x02",
|
||||
0xD064E: b"\x00",
|
||||
0xD06F6: b"\x02",
|
||||
0xD0882: b"\x00",
|
||||
0xD0912: b"\x02",
|
||||
0xD0C2A: b"\x02",
|
||||
0xD0C96: b"\x01",
|
||||
0xD0D92: b"\x02",
|
||||
0xD0DCE: b"\x01",
|
||||
0xD1332: b"\x00",
|
||||
0xD13AA: b"\x01",
|
||||
0xD1722: b"\x02",
|
||||
0xD17A6: b"\x01",
|
||||
0xD1926: b"\x01",
|
||||
0xD19AA: b"\x02",
|
||||
0xD1A9A: b"\x02",
|
||||
0xD1AA6: b"\x00",
|
||||
0xD1EBA: b"\x00",
|
||||
0xD1ED2: b"\x01",
|
||||
0xD2262: b"\x02",
|
||||
0xD23B2: b"\x03",
|
||||
0xD256E: b"\x02",
|
||||
0xD2742: b"\x02",
|
||||
0xD2832: b"\x04",
|
||||
0xD2862: b"\x01",
|
||||
0xD2A2A: b"\x01",
|
||||
0xD2DBA: b"\x04",
|
||||
0xD2DC6: b"\x00",
|
||||
0xD2E02: b"\x02",
|
||||
0xD2EFE: b"\x04",
|
||||
0xD2F0A: b"\x02",
|
||||
0xD302A: b"\x00",
|
||||
0xD3042: b"\x01",
|
||||
0xD304E: b"\x04",
|
||||
0xD3066: b"\x02",
|
||||
0xD322E: b"\x04",
|
||||
0xD334E: b"\x04",
|
||||
0xD3516: b"\x03",
|
||||
0xD35CA: b"\x02",
|
||||
0xD371A: b"\x01",
|
||||
0xD38EE: b"\x00",
|
||||
0xD3BE2: b"\x02",
|
||||
0xD3D1A: b"\x01",
|
||||
0xD3D56: b"\x02",
|
||||
0xD3ECA: b"\x00",
|
||||
0xD3EE2: b"\x02",
|
||||
0xD4056: b"\x01",
|
||||
0xD40E6: b"\x04",
|
||||
0xD413A: b"\x04",
|
||||
0xD4326: b"\x00",
|
||||
0xD460E: b"\x00",
|
||||
0xD48D2: b"\x00",
|
||||
0xD49E6: b"\x01",
|
||||
0xD4ABE: b"\x02",
|
||||
0xD4B8A: b"\x01",
|
||||
0xD4D0A: b"\x04",
|
||||
0xD4EAE: b"\x02",
|
||||
0xD4F0E: b"\x00",
|
||||
0xD4F92: b"\x02",
|
||||
0xD4FB6: b"\x01",
|
||||
0xD503A: b"\x03",
|
||||
0xD5646: b"\x01",
|
||||
0xD5682: b"\x02",
|
||||
0xD57C6: b"\x02",
|
||||
0xD57D2: b"\x02",
|
||||
0xD58F2: b"\x00",
|
||||
0xD5922: b"\x01",
|
||||
0xD5B9E: b"\x02",
|
||||
0xD5E26: b"\x01",
|
||||
0xD5E56: b"\x02",
|
||||
0xD5E7A: b"\x02",
|
||||
0xD5F5E: b"\x00",
|
||||
0xD69EA: b"\x02",
|
||||
0xD69F6: b"\x01",
|
||||
0xD6A02: b"\x00",
|
||||
0xD6A0E: b"\x04",
|
||||
0xD6A1A: b"\x03",
|
||||
0xD6BE2: b"\x00",
|
||||
0xD6CBA: b"\x01",
|
||||
0xD6CDE: b"\x02",
|
||||
0xD6EEE: b"\x00",
|
||||
0xD6F1E: b"\x02",
|
||||
0xD6F42: b"\x01",
|
||||
0xD6FC6: b"\x04",
|
||||
0xD706E: b"\x00",
|
||||
0xD716A: b"\x02",
|
||||
0xD72AE: b"\x01",
|
||||
0xD75BA: b"\x03",
|
||||
0xD76AA: b"\x04",
|
||||
0xD76B6: b"\x00",
|
||||
0xD76C2: b"\x01",
|
||||
0xD76CE: b"\x02",
|
||||
0xD76DA: b"\x03",
|
||||
0xD7D46: b"\x00",
|
||||
0xD7D52: b"\x00",
|
||||
}
|
||||
|
||||
LOW_ITEMS = [
|
||||
41, # Potion
|
||||
42, # Meat
|
||||
48, # Mind Restore
|
||||
51, # Heart
|
||||
46, # Antidote
|
||||
47, # Cure Curse
|
||||
|
||||
17, # Cotton Clothes
|
||||
18, # Prison Garb
|
||||
12, # Cotton Robe
|
||||
1, # Leather Armor
|
||||
2, # Bronze Armor
|
||||
3, # Gold Armor
|
||||
|
||||
39, # Toy Ring
|
||||
40, # Bear Ring
|
||||
34, # Wristband
|
||||
36, # Arm Guard
|
||||
37, # Magic Gauntlet
|
||||
38, # Miracle Armband
|
||||
35, # Gauntlet
|
||||
]
|
||||
|
||||
MID_ITEMS = [
|
||||
43, # Spiced Meat
|
||||
49, # Mind High
|
||||
52, # Heart High
|
||||
|
||||
19, # Stylish Suit
|
||||
20, # Night Suit
|
||||
13, # Silk Robe
|
||||
14, # Rainbow Robe
|
||||
4, # Chainmail
|
||||
5, # Steel Armor
|
||||
6, # Platinum Armor
|
||||
|
||||
24, # Star Bracelet
|
||||
29, # Cursed Ring
|
||||
25, # Strength Ring
|
||||
26, # Hard Ring
|
||||
27, # Intelligence Ring
|
||||
28, # Luck Ring
|
||||
23, # Double Grips
|
||||
]
|
||||
|
||||
HIGH_ITEMS = [
|
||||
44, # Potion High
|
||||
45, # Potion Ex
|
||||
50, # Mind Ex
|
||||
53, # Heart Ex
|
||||
54, # Heart Mega
|
||||
|
||||
21, # Ninja Garb
|
||||
22, # Soldier Fatigues
|
||||
15, # Magic Robe
|
||||
16, # Sage Robe
|
||||
|
||||
7, # Diamond Armor
|
||||
8, # Mirror Armor
|
||||
9, # Needle Armor
|
||||
10, # Dark Armor
|
||||
|
||||
30, # Strength Armband
|
||||
31, # Defense Armband
|
||||
32, # Sage Armband
|
||||
33, # Gambler Armband
|
||||
]
|
||||
|
||||
COMMON_ITEMS = LOW_ITEMS + MID_ITEMS
|
||||
|
||||
RARE_ITEMS = LOW_ITEMS + MID_ITEMS + HIGH_ITEMS
|
||||
|
||||
|
||||
class CVCotMEnemyData(NamedTuple):
|
||||
name: str
|
||||
hp: int
|
||||
attack: int
|
||||
defense: int
|
||||
exp: int
|
||||
type: Optional[str] = None
|
||||
|
||||
|
||||
cvcotm_enemy_info: List[CVCotMEnemyData] = [
|
||||
# Name HP ATK DEF EXP
|
||||
CVCotMEnemyData("Medusa Head", 6, 120, 60, 2),
|
||||
CVCotMEnemyData("Zombie", 48, 70, 20, 2),
|
||||
CVCotMEnemyData("Ghoul", 100, 190, 79, 3),
|
||||
CVCotMEnemyData("Wight", 110, 235, 87, 4),
|
||||
CVCotMEnemyData("Clinking Man", 80, 135, 25, 21),
|
||||
CVCotMEnemyData("Zombie Thief", 120, 185, 30, 58),
|
||||
CVCotMEnemyData("Skeleton", 25, 65, 45, 4),
|
||||
CVCotMEnemyData("Skeleton Bomber", 20, 50, 40, 4),
|
||||
CVCotMEnemyData("Electric Skeleton", 42, 80, 50, 30),
|
||||
CVCotMEnemyData("Skeleton Spear", 30, 65, 46, 6),
|
||||
CVCotMEnemyData("Skeleton Boomerang", 60, 170, 90, 112),
|
||||
CVCotMEnemyData("Skeleton Soldier", 35, 90, 60, 16),
|
||||
CVCotMEnemyData("Skeleton Knight", 50, 140, 80, 39),
|
||||
CVCotMEnemyData("Bone Tower", 84, 201, 280, 160),
|
||||
CVCotMEnemyData("Fleaman", 60, 142, 45, 29),
|
||||
CVCotMEnemyData("Poltergeist", 105, 360, 380, 510),
|
||||
CVCotMEnemyData("Bat", 5, 50, 15, 4),
|
||||
CVCotMEnemyData("Spirit", 9, 55, 17, 1),
|
||||
CVCotMEnemyData("Ectoplasm", 12, 165, 51, 2),
|
||||
CVCotMEnemyData("Specter", 15, 295, 95, 3),
|
||||
CVCotMEnemyData("Axe Armor", 55, 120, 130, 31),
|
||||
CVCotMEnemyData("Flame Armor", 160, 320, 300, 280),
|
||||
CVCotMEnemyData("Flame Demon", 300, 315, 270, 600),
|
||||
CVCotMEnemyData("Ice Armor", 240, 470, 520, 1500),
|
||||
CVCotMEnemyData("Thunder Armor", 204, 340, 320, 800),
|
||||
CVCotMEnemyData("Wind Armor", 320, 500, 460, 1800),
|
||||
CVCotMEnemyData("Earth Armor", 130, 230, 280, 240),
|
||||
CVCotMEnemyData("Poison Armor", 260, 382, 310, 822),
|
||||
CVCotMEnemyData("Forest Armor", 370, 390, 390, 1280),
|
||||
CVCotMEnemyData("Stone Armor", 90, 220, 320, 222),
|
||||
CVCotMEnemyData("Ice Demon", 350, 492, 510, 4200),
|
||||
CVCotMEnemyData("Holy Armor", 350, 420, 450, 1700),
|
||||
CVCotMEnemyData("Thunder Demon", 180, 270, 230, 450),
|
||||
CVCotMEnemyData("Dark Armor", 400, 680, 560, 3300),
|
||||
CVCotMEnemyData("Wind Demon", 400, 540, 490, 3600),
|
||||
CVCotMEnemyData("Bloody Sword", 30, 220, 500, 200),
|
||||
CVCotMEnemyData("Golem", 650, 520, 700, 1400),
|
||||
CVCotMEnemyData("Earth Demon", 150, 90, 85, 25),
|
||||
CVCotMEnemyData("Were-wolf", 160, 265, 110, 140),
|
||||
CVCotMEnemyData("Man Eater", 400, 330, 233, 700),
|
||||
CVCotMEnemyData("Devil Tower", 10, 140, 200, 17),
|
||||
CVCotMEnemyData("Skeleton Athlete", 100, 100, 50, 25),
|
||||
CVCotMEnemyData("Harpy", 120, 275, 200, 271),
|
||||
CVCotMEnemyData("Siren", 160, 443, 300, 880),
|
||||
CVCotMEnemyData("Imp", 90, 220, 99, 103),
|
||||
CVCotMEnemyData("Mudman", 25, 79, 30, 2),
|
||||
CVCotMEnemyData("Gargoyle", 60, 160, 66, 3),
|
||||
CVCotMEnemyData("Slime", 40, 102, 18, 11),
|
||||
CVCotMEnemyData("Frozen Shade", 112, 490, 560, 1212),
|
||||
CVCotMEnemyData("Heat Shade", 80, 240, 200, 136),
|
||||
CVCotMEnemyData("Poison Worm", 120, 30, 20, 12),
|
||||
CVCotMEnemyData("Myconid", 50, 250, 114, 25),
|
||||
CVCotMEnemyData("Will O'Wisp", 11, 110, 16, 9),
|
||||
CVCotMEnemyData("Spearfish", 40, 360, 450, 280),
|
||||
CVCotMEnemyData("Merman", 60, 303, 301, 10),
|
||||
CVCotMEnemyData("Minotaur", 410, 520, 640, 2000),
|
||||
CVCotMEnemyData("Were-horse", 400, 540, 360, 1970),
|
||||
CVCotMEnemyData("Marionette", 80, 160, 150, 127),
|
||||
CVCotMEnemyData("Gremlin", 30, 80, 33, 2),
|
||||
CVCotMEnemyData("Hopper", 40, 87, 35, 8),
|
||||
CVCotMEnemyData("Evil Pillar", 20, 460, 800, 480),
|
||||
CVCotMEnemyData("Were-panther", 200, 300, 130, 270),
|
||||
CVCotMEnemyData("Were-jaguar", 270, 416, 170, 760),
|
||||
CVCotMEnemyData("Bone Head", 24, 60, 80, 7),
|
||||
CVCotMEnemyData("Fox Archer", 75, 130, 59, 53),
|
||||
CVCotMEnemyData("Fox Hunter", 100, 290, 140, 272),
|
||||
CVCotMEnemyData("Were-bear", 265, 250, 140, 227),
|
||||
CVCotMEnemyData("Grizzly", 600, 380, 200, 960),
|
||||
CVCotMEnemyData("Cerberus", 600, 150, 100, 500, "boss"),
|
||||
CVCotMEnemyData("Beast Demon", 150, 330, 250, 260),
|
||||
CVCotMEnemyData("Arch Demon", 320, 505, 400, 1000),
|
||||
CVCotMEnemyData("Demon Lord", 460, 660, 500, 1950),
|
||||
CVCotMEnemyData("Gorgon", 230, 215, 165, 219),
|
||||
CVCotMEnemyData("Catoblepas", 550, 500, 430, 1800),
|
||||
CVCotMEnemyData("Succubus", 150, 400, 350, 710),
|
||||
CVCotMEnemyData("Fallen Angel", 370, 770, 770, 6000),
|
||||
CVCotMEnemyData("Necromancer", 500, 200, 250, 2500, "boss"),
|
||||
CVCotMEnemyData("Hyena", 93, 140, 70, 105),
|
||||
CVCotMEnemyData("Fishhead", 80, 320, 504, 486),
|
||||
CVCotMEnemyData("Dryad", 120, 300, 360, 300),
|
||||
CVCotMEnemyData("Mimic Candle", 990, 600, 600, 6600, "candle"),
|
||||
CVCotMEnemyData("Brain Float", 20, 50, 25, 10),
|
||||
CVCotMEnemyData("Evil Hand", 52, 150, 120, 63),
|
||||
CVCotMEnemyData("Abiondarg", 88, 388, 188, 388),
|
||||
CVCotMEnemyData("Iron Golem", 640, 290, 450, 8000, "boss"),
|
||||
CVCotMEnemyData("Devil", 1080, 800, 900, 10000),
|
||||
CVCotMEnemyData("Witch", 144, 330, 290, 600),
|
||||
CVCotMEnemyData("Mummy", 100, 100, 35, 3),
|
||||
CVCotMEnemyData("Hipogriff", 300, 500, 210, 740),
|
||||
CVCotMEnemyData("Adramelech", 1800, 380, 360, 16000, "boss"),
|
||||
CVCotMEnemyData("Arachne", 330, 420, 288, 1300),
|
||||
CVCotMEnemyData("Death Mantis", 200, 318, 240, 400),
|
||||
CVCotMEnemyData("Alraune", 774, 490, 303, 2500),
|
||||
CVCotMEnemyData("King Moth", 140, 290, 160, 150),
|
||||
CVCotMEnemyData("Killer Bee", 8, 308, 108, 88),
|
||||
CVCotMEnemyData("Dragon Zombie", 1400, 390, 440, 15000, "boss"),
|
||||
CVCotMEnemyData("Lizardman", 100, 345, 400, 800),
|
||||
CVCotMEnemyData("Franken", 1200, 700, 350, 2100),
|
||||
CVCotMEnemyData("Legion", 420, 610, 375, 1590),
|
||||
CVCotMEnemyData("Dullahan", 240, 550, 440, 2200),
|
||||
CVCotMEnemyData("Death", 880, 600, 800, 60000, "boss"),
|
||||
CVCotMEnemyData("Camilla", 1500, 650, 700, 80000, "boss"),
|
||||
CVCotMEnemyData("Hugh", 1400, 570, 750, 120000, "boss"),
|
||||
CVCotMEnemyData("Dracula", 1100, 805, 850, 150000, "boss"),
|
||||
CVCotMEnemyData("Dracula", 3000, 1000, 1000, 0, "final boss"),
|
||||
CVCotMEnemyData("Skeleton Medalist", 250, 100, 100, 1500),
|
||||
CVCotMEnemyData("Were-jaguar", 320, 518, 260, 1200, "battle arena"),
|
||||
CVCotMEnemyData("Were-wolf", 340, 525, 180, 1100, "battle arena"),
|
||||
CVCotMEnemyData("Catoblepas", 560, 510, 435, 2000, "battle arena"),
|
||||
CVCotMEnemyData("Hipogriff", 500, 620, 280, 1900, "battle arena"),
|
||||
CVCotMEnemyData("Wind Demon", 490, 600, 540, 4000, "battle arena"),
|
||||
CVCotMEnemyData("Witch", 210, 480, 340, 1000, "battle arena"),
|
||||
CVCotMEnemyData("Stone Armor", 260, 585, 750, 3000, "battle arena"),
|
||||
CVCotMEnemyData("Devil Tower", 50, 560, 700, 600, "battle arena"),
|
||||
CVCotMEnemyData("Skeleton", 150, 400, 200, 500, "battle arena"),
|
||||
CVCotMEnemyData("Skeleton Bomber", 150, 400, 200, 550, "battle arena"),
|
||||
CVCotMEnemyData("Electric Skeleton", 150, 400, 200, 700, "battle arena"),
|
||||
CVCotMEnemyData("Skeleton Spear", 150, 400, 200, 580, "battle arena"),
|
||||
CVCotMEnemyData("Flame Demon", 680, 650, 600, 4500, "battle arena"),
|
||||
CVCotMEnemyData("Bone Tower", 120, 500, 650, 800, "battle arena"),
|
||||
CVCotMEnemyData("Fox Hunter", 160, 510, 220, 600, "battle arena"),
|
||||
CVCotMEnemyData("Poison Armor", 380, 680, 634, 3600, "battle arena"),
|
||||
CVCotMEnemyData("Bloody Sword", 55, 600, 1200, 2000, "battle arena"),
|
||||
CVCotMEnemyData("Abiondarg", 188, 588, 288, 588, "battle arena"),
|
||||
CVCotMEnemyData("Legion", 540, 760, 480, 2900, "battle arena"),
|
||||
CVCotMEnemyData("Marionette", 200, 420, 400, 1200, "battle arena"),
|
||||
CVCotMEnemyData("Minotaur", 580, 700, 715, 4100, "battle arena"),
|
||||
CVCotMEnemyData("Arachne", 430, 590, 348, 2400, "battle arena"),
|
||||
CVCotMEnemyData("Succubus", 300, 670, 630, 3100, "battle arena"),
|
||||
CVCotMEnemyData("Demon Lord", 590, 800, 656, 4200, "battle arena"),
|
||||
CVCotMEnemyData("Alraune", 1003, 640, 450, 5000, "battle arena"),
|
||||
CVCotMEnemyData("Hyena", 210, 408, 170, 1000, "battle arena"),
|
||||
CVCotMEnemyData("Devil Armor", 500, 804, 714, 6600),
|
||||
CVCotMEnemyData("Evil Pillar", 55, 655, 900, 1500, "battle arena"),
|
||||
CVCotMEnemyData("White Armor", 640, 770, 807, 7000),
|
||||
CVCotMEnemyData("Devil", 1530, 980, 1060, 30000, "battle arena"),
|
||||
CVCotMEnemyData("Scary Candle", 150, 300, 300, 900, "candle"),
|
||||
CVCotMEnemyData("Trick Candle", 200, 400, 400, 1400, "candle"),
|
||||
CVCotMEnemyData("Nightmare", 250, 550, 550, 2000),
|
||||
CVCotMEnemyData("Lilim", 400, 800, 800, 8000),
|
||||
CVCotMEnemyData("Lilith", 660, 960, 960, 20000),
|
||||
]
|
||||
# NOTE: Coffin is omitted from the end of this, as its presence doesn't
|
||||
# actually impact the randomizer (all stats and drops inherited from Mummy).
|
||||
|
||||
BOSS_IDS = [enemy_id for enemy_id in range(len(cvcotm_enemy_info)) if cvcotm_enemy_info[enemy_id].type == "boss"]
|
||||
|
||||
ENEMY_TABLE_START = 0xCB2C4
|
||||
|
||||
NUMBER_ITEMS = 55
|
||||
|
||||
COUNTDOWN_TABLE_ADDR = 0x673400
|
||||
ITEM_ID_SHINNING_ARMOR = 11
|
||||
|
||||
|
||||
def shuffle_sub_weapons(world: "CVCotMWorld") -> Dict[int, bytes]:
|
||||
"""Shuffles the sub-weapons amongst themselves."""
|
||||
sub_bytes = list(rom_sub_weapon_offsets.values())
|
||||
world.random.shuffle(sub_bytes)
|
||||
return dict(zip(rom_sub_weapon_offsets, sub_bytes))
|
||||
|
||||
|
||||
def get_countdown_flags(world: "CVCotMWorld", active_locations: Iterable[Location]) -> Dict[int, bytes]:
|
||||
"""Figures out which Countdown numbers to increase for each Location after verifying the Item on the Location should
|
||||
count towards a number.
|
||||
|
||||
Which number to increase is determined by the Location's "countdown" attr in its CVCotMLocationData."""
|
||||
|
||||
next_pos = COUNTDOWN_TABLE_ADDR + 0x40
|
||||
countdown_flags: List[List[int]] = [[] for _ in range(16)]
|
||||
countdown_dict = {}
|
||||
ptr_offset = COUNTDOWN_TABLE_ADDR
|
||||
|
||||
# Loop over every Location.
|
||||
for loc in active_locations:
|
||||
# If the Location's Item is not Progression/Useful-classified with the "Majors" Countdown being used, or if the
|
||||
# Location is the Iron Maiden switch with the vanilla Iron Maiden behavior, skip adding its flag to the arrays.
|
||||
if (not loc.item.classification & MAJORS_CLASSIFICATIONS and world.options.countdown ==
|
||||
Countdown.option_majors):
|
||||
continue
|
||||
|
||||
countdown_index = cvcotm_location_info[loc.name].countdown
|
||||
# Take the Location's address if the above condition is satisfied, and get the flag value out of it.
|
||||
countdown_flags[countdown_index] += [loc.address & 0xFF, 0]
|
||||
|
||||
# Write the Countdown flag arrays and array pointers correctly. Each flag list should end with a 0xFFFF to indicate
|
||||
# the end of an area's list.
|
||||
for area_flags in countdown_flags:
|
||||
countdown_dict[ptr_offset] = int.to_bytes(next_pos | 0x08000000, 4, "little")
|
||||
countdown_dict[next_pos] = bytes(area_flags + [0xFF, 0xFF])
|
||||
ptr_offset += 4
|
||||
next_pos += len(area_flags) + 2
|
||||
|
||||
return countdown_dict
|
||||
|
||||
|
||||
def get_location_data(world: "CVCotMWorld", active_locations: Iterable[Location]) -> Dict[int, bytes]:
|
||||
"""Gets ALL the Item data to go into the ROM. Items consist of four bytes; the first two represent the object ID
|
||||
for the "category" of item that it belongs to, the third is the sub-value for which item within that "category" it
|
||||
is, and the fourth controls the appearance it takes."""
|
||||
|
||||
location_bytes = {}
|
||||
|
||||
for loc in active_locations:
|
||||
# Figure out the item ID bytes to put in each Location's offset here.
|
||||
# If it's a CotM Item, always write the Item's primary type byte.
|
||||
if loc.item.game == "Castlevania - Circle of the Moon":
|
||||
type_byte = cvcotm_item_info[loc.item.name].code >> 8
|
||||
|
||||
# If the Item is for this player, set the subtype to actually be that Item.
|
||||
# Otherwise, set a dummy subtype value that is different for every item type.
|
||||
if loc.item.player == world.player:
|
||||
subtype_byte = cvcotm_item_info[loc.item.name].code & 0xFF
|
||||
else:
|
||||
subtype_byte = other_player_subtype_bytes[type_byte]
|
||||
|
||||
# If it's a DSS Card, set the appearance based on whether it's progression or not; freeze combo cards should
|
||||
# all appear blue in color while the others are standard purple/yellow. Otherwise, set the appearance the
|
||||
# same way as the subtype for local items regardless of whether it's actually local or not.
|
||||
if type_byte == 0xE6:
|
||||
if loc.item.advancement:
|
||||
appearance_byte = 1
|
||||
else:
|
||||
appearance_byte = 0
|
||||
else:
|
||||
appearance_byte = cvcotm_item_info[loc.item.name].code & 0xFF
|
||||
|
||||
# If it's not a CotM Item at all, always set the primary type to that of a Magic Item and the subtype to that of
|
||||
# a dummy item. The AP Items are all under Magic Items.
|
||||
else:
|
||||
type_byte = 0xE8
|
||||
subtype_byte = 0x0A
|
||||
# Decide which AP Item to use to represent the other game item.
|
||||
if loc.item.classification & ItemClassification.progression and \
|
||||
loc.item.classification & ItemClassification.useful:
|
||||
appearance_byte = 0x0E # Progression + Useful
|
||||
elif loc.item.classification & ItemClassification.progression:
|
||||
appearance_byte = 0x0C # Progression
|
||||
elif loc.item.classification & ItemClassification.useful:
|
||||
appearance_byte = 0x0B # Useful
|
||||
elif loc.item.classification & ItemClassification.trap:
|
||||
appearance_byte = 0x0D # Trap
|
||||
else:
|
||||
appearance_byte = 0x0A # Filler
|
||||
|
||||
# Check if the Item's game is in the other game item appearances' dict, and if so, if the Item is under that
|
||||
# game's name. If it is, change the appearance accordingly.
|
||||
# Right now, only SotN and Timespinner stat ups are supported.
|
||||
other_game_name = world.multiworld.worlds[loc.item.player].game
|
||||
if other_game_name in other_game_item_appearances:
|
||||
if loc.item.name in other_game_item_appearances[other_game_name]:
|
||||
type_byte = other_game_item_appearances[other_game_name][loc.item.name]["type"]
|
||||
subtype_byte = other_player_subtype_bytes[type_byte]
|
||||
appearance_byte = other_game_item_appearances[other_game_name][loc.item.name]["appearance"]
|
||||
|
||||
# Create the correct bytes object for the Item on that Location.
|
||||
location_bytes[cvcotm_location_info[loc.name].offset] = bytes([type_byte, 1, subtype_byte, appearance_byte])
|
||||
return location_bytes
|
||||
|
||||
|
||||
def populate_enemy_drops(world: "CVCotMWorld") -> Dict[int, bytes]:
|
||||
"""Randomizes the enemy-dropped items throughout the game within each other. There are three tiers of item drops:
|
||||
Low, Mid, and High. Each enemy has two item slots that can both drop its own item; a Common slot and a Rare one.
|
||||
|
||||
On Normal item randomization, easy enemies (below 61 HP) will only have Low-tier drops in both of their stats,
|
||||
bosses and candle enemies will be guaranteed to have High drops in one or both of their slots respectively (bosses
|
||||
are made to only drop one slot 100% of the time), and everything else can have a Low or Mid-tier item in its Common
|
||||
drop slot and a Low, Mid, OR High-tier item in its Rare drop slot.
|
||||
|
||||
If Item Drop Randomization is set to Tiered, the HP threshold for enemies being considered "easily" will raise to
|
||||
below 144, enemies in the 144-369 HP range (inclusive) will have a Low-tier item in its Common slot and a Mid-tier
|
||||
item in its rare slot, and enemies with more than 369 HP will have a Mid-tier in its Common slot and a High-tier in
|
||||
its Rare slot. Candles and bosses still have Rares in all their slots, but now the guaranteed drops that land on
|
||||
bosses will be exclusive to them; no other enemy in the game will have their item.
|
||||
|
||||
This and select_drop are the most directly adapted code from upstream CotMR in this package by far. Credit where
|
||||
it's due to Spooky for writing the original, and Malaert64 for further refinements and updating what used to be
|
||||
Random Item Hardmode to instead be Tiered Item Mode. The original C code this was adapted from can be found here:
|
||||
https://github.com/calm-palm/cotm-randomizer/blob/master/Program/randomizer.c#L1028"""
|
||||
|
||||
placed_low_items = [0] * len(LOW_ITEMS)
|
||||
placed_mid_items = [0] * len(MID_ITEMS)
|
||||
placed_high_items = [0] * len(HIGH_ITEMS)
|
||||
|
||||
placed_common_items = [0] * len(COMMON_ITEMS)
|
||||
placed_rare_items = [0] * len(RARE_ITEMS)
|
||||
|
||||
regular_drops = [0] * len(cvcotm_enemy_info)
|
||||
regular_drop_chances = [0] * len(cvcotm_enemy_info)
|
||||
rare_drops = [0] * len(cvcotm_enemy_info)
|
||||
rare_drop_chances = [0] * len(cvcotm_enemy_info)
|
||||
|
||||
# Set boss items first to prevent boss drop duplicates.
|
||||
# If Tiered mode is enabled, make these items exclusive to these enemies by adding an arbitrary integer larger
|
||||
# than could be reached normally (e.g.the total number of enemies) and use the placed high items array instead of
|
||||
# the placed rare items one.
|
||||
if world.options.item_drop_randomization == ItemDropRandomization.option_tiered:
|
||||
for boss_id in BOSS_IDS:
|
||||
regular_drops[boss_id] = select_drop(world, HIGH_ITEMS, placed_high_items, True)
|
||||
else:
|
||||
for boss_id in BOSS_IDS:
|
||||
regular_drops[boss_id] = select_drop(world, RARE_ITEMS, placed_rare_items, start_index=len(COMMON_ITEMS))
|
||||
|
||||
# Setting drop logic for all enemies.
|
||||
for i in range(len(cvcotm_enemy_info)):
|
||||
|
||||
# Give Dracula II Shining Armor occasionally as a joke.
|
||||
if cvcotm_enemy_info[i].type == "final boss":
|
||||
regular_drops[i] = rare_drops[i] = ITEM_ID_SHINNING_ARMOR
|
||||
regular_drop_chances[i] = rare_drop_chances[i] = 5000
|
||||
|
||||
# Set bosses' secondary item to none since we already set the primary item earlier.
|
||||
elif cvcotm_enemy_info[i].type == "boss":
|
||||
# Set rare drop to none.
|
||||
rare_drops[i] = 0
|
||||
|
||||
# Max out rare boss drops (normally, drops are capped to 50% and 25% for common and rare respectively, but
|
||||
# Fuse's patch AllowAlwaysDrop.ips allows setting the regular item drop chance to 10000 to force a drop
|
||||
# always)
|
||||
regular_drop_chances[i] = 10000
|
||||
rare_drop_chances[i] = 0
|
||||
|
||||
# Candle enemies use a similar placement logic to the bosses, except items that land on them are NOT exclusive
|
||||
# to them on Tiered mode.
|
||||
elif cvcotm_enemy_info[i].type == "candle":
|
||||
if world.options.item_drop_randomization == ItemDropRandomization.option_tiered:
|
||||
regular_drops[i] = select_drop(world, HIGH_ITEMS, placed_high_items)
|
||||
rare_drops[i] = select_drop(world, HIGH_ITEMS, placed_high_items)
|
||||
else:
|
||||
regular_drops[i] = select_drop(world, RARE_ITEMS, placed_rare_items, start_index=len(COMMON_ITEMS))
|
||||
rare_drops[i] = select_drop(world, RARE_ITEMS, placed_rare_items, start_index=len(COMMON_ITEMS))
|
||||
|
||||
# Set base drop chances at 20-30% for common and 15-20% for rare.
|
||||
regular_drop_chances[i] = 2000 + world.random.randint(0, 1000)
|
||||
rare_drop_chances[i] = 1500 + world.random.randint(0, 500)
|
||||
|
||||
# On All Bosses and Battle Arena Required, the Shinning Armor at the end of Battle Arena is removed.
|
||||
# We compensate for this by giving the Battle Arena Devil a 100% chance to drop Shinning Armor.
|
||||
elif cvcotm_enemy_info[i].name == "Devil" and cvcotm_enemy_info[i].type == "battle arena" and \
|
||||
world.options.required_skirmishes == RequiredSkirmishes.option_all_bosses_and_arena:
|
||||
regular_drops[i] = ITEM_ID_SHINNING_ARMOR
|
||||
rare_drops[i] = 0
|
||||
|
||||
regular_drop_chances[i] = 10000
|
||||
rare_drop_chances[i] = 0
|
||||
|
||||
# Low-tier items drop from enemies that are trivial to farm (60 HP or less)
|
||||
# on Normal drop logic, or enemies under 144 HP on Tiered logic.
|
||||
elif (world.options.item_drop_randomization == ItemDropRandomization.option_normal and
|
||||
cvcotm_enemy_info[i].hp <= 60) or \
|
||||
(world.options.item_drop_randomization == ItemDropRandomization.option_tiered and
|
||||
cvcotm_enemy_info[i].hp <= 143):
|
||||
# Low-tier enemy drops.
|
||||
regular_drops[i] = select_drop(world, LOW_ITEMS, placed_low_items)
|
||||
rare_drops[i] = select_drop(world, LOW_ITEMS, placed_low_items)
|
||||
|
||||
# Set base drop chances at 6-10% for common and 3-6% for rare.
|
||||
regular_drop_chances[i] = 600 + world.random.randint(0, 400)
|
||||
rare_drop_chances[i] = 300 + world.random.randint(0, 300)
|
||||
|
||||
# Rest of Tiered logic, by Malaert64.
|
||||
elif world.options.item_drop_randomization == ItemDropRandomization.option_tiered:
|
||||
# If under 370 HP, mid-tier enemy.
|
||||
if cvcotm_enemy_info[i].hp <= 369:
|
||||
regular_drops[i] = select_drop(world, LOW_ITEMS, placed_low_items)
|
||||
rare_drops[i] = select_drop(world, MID_ITEMS, placed_mid_items)
|
||||
# Otherwise, enemy HP is 370+, thus high-tier enemy.
|
||||
else:
|
||||
regular_drops[i] = select_drop(world, MID_ITEMS, placed_mid_items)
|
||||
rare_drops[i] = select_drop(world, HIGH_ITEMS, placed_high_items)
|
||||
|
||||
# Set base drop chances at 6-10% for common and 3-6% for rare.
|
||||
regular_drop_chances[i] = 600 + world.random.randint(0, 400)
|
||||
rare_drop_chances[i] = 300 + world.random.randint(0, 300)
|
||||
|
||||
# Regular enemies outside Tiered logic.
|
||||
else:
|
||||
# Select a random regular and rare drop for every enemy from their respective lists.
|
||||
regular_drops[i] = select_drop(world, COMMON_ITEMS, placed_common_items)
|
||||
rare_drops[i] = select_drop(world, RARE_ITEMS, placed_rare_items)
|
||||
|
||||
# Set base drop chances at 6-10% for common and 3-6% for rare.
|
||||
regular_drop_chances[i] = 600 + world.random.randint(0, 400)
|
||||
rare_drop_chances[i] = 300 + world.random.randint(0, 300)
|
||||
|
||||
# Return the randomized drop data as bytes with their respective offsets.
|
||||
enemy_address = ENEMY_TABLE_START
|
||||
drop_data = {}
|
||||
for i, enemy_info in enumerate(cvcotm_enemy_info):
|
||||
drop_data[enemy_address] = bytes([regular_drops[i], 0, regular_drop_chances[i] & 0xFF,
|
||||
regular_drop_chances[i] >> 8, rare_drops[i], 0, rare_drop_chances[i] & 0xFF,
|
||||
rare_drop_chances[i] >> 8])
|
||||
enemy_address += 20
|
||||
|
||||
return drop_data
|
||||
|
||||
|
||||
def select_drop(world: "CVCotMWorld", drop_list: List[int], drops_placed: List[int], exclusive_drop: bool = False,
|
||||
start_index: int = 0) -> int:
|
||||
"""Chooses a drop from a given list of drops based on another given list of how many drops from that list were
|
||||
selected before. In order to ensure an even number of drops are distributed, drops that were selected the least are
|
||||
the ones that will be picked from.
|
||||
|
||||
Calling this with exclusive_drop param being True will force the number of the chosen item really high to ensure it
|
||||
will never be picked again."""
|
||||
|
||||
# Take the list of placed item drops beginning from the starting index.
|
||||
drops_from_start_index = drops_placed[start_index:]
|
||||
|
||||
# Determine the lowest drop counts and the indices with that drop count.
|
||||
lowest_number = min(drops_from_start_index)
|
||||
indices_with_lowest_number = [index for index, placed in enumerate(drops_from_start_index) if
|
||||
placed == lowest_number]
|
||||
|
||||
random_index = world.random.choice(indices_with_lowest_number)
|
||||
random_index += start_index # Add start_index back on
|
||||
|
||||
# Increment the number of this item placed, unless it should be exclusive to the boss / candle, in which case
|
||||
# set it to an arbitrarily large number to make it exclusive.
|
||||
if exclusive_drop:
|
||||
drops_placed[random_index] += 999
|
||||
else:
|
||||
drops_placed[random_index] += 1
|
||||
|
||||
# Return the in-game item ID of the chosen item.
|
||||
return drop_list[random_index]
|
||||
|
||||
|
||||
def get_start_inventory_data(world: "CVCotMWorld") -> Tuple[Dict[int, bytes], bool]:
|
||||
"""Calculate and return the starting inventory arrays. Different items go into different arrays, so they all have
|
||||
to be handled accordingly."""
|
||||
start_inventory_data = {}
|
||||
|
||||
magic_items_array = [0 for _ in range(8)]
|
||||
cards_array = [0 for _ in range(20)]
|
||||
extra_stats = {"extra health": 0,
|
||||
"extra magic": 0,
|
||||
"extra hearts": 0}
|
||||
start_with_detonator = False
|
||||
# If the Iron Maiden Behavior option is set to Start Broken, consider ourselves starting with the Maiden Detonator.
|
||||
if world.options.iron_maiden_behavior == IronMaidenBehavior.option_start_broken:
|
||||
start_with_detonator = True
|
||||
|
||||
# Always start with the Dash Boots.
|
||||
magic_items_array[0] = 1
|
||||
|
||||
for item in world.multiworld.precollected_items[world.player]:
|
||||
|
||||
array_offset = item.code & 0xFF
|
||||
|
||||
# If it's a Maiden Detonator we're starting with, set the boolean for it to True.
|
||||
if item.name == iname.ironmaidens:
|
||||
start_with_detonator = True
|
||||
# If it's a Max Up we're starting with, check if increasing the extra amount of that stat will put us over the
|
||||
# max amount of the stat allowed. If it will, set the current extra amount to the max. Otherwise, increase it by
|
||||
# the amount that it should.
|
||||
elif "Max Up" in item.name:
|
||||
info = extra_starting_stat_info[item.name]
|
||||
if extra_stats[info["variable"]] + info["amount_per"] > info["max_allowed"]:
|
||||
extra_stats[info["variable"]] = info["max_allowed"]
|
||||
else:
|
||||
extra_stats[info["variable"]] += info["amount_per"]
|
||||
# If it's a DSS card we're starting with, set that card's value in the cards array.
|
||||
elif "Card" in item.name:
|
||||
cards_array[array_offset] = 1
|
||||
# If it's none of the above, it has to be a regular Magic Item.
|
||||
# Increase that Magic Item's value in the Magic Items array if it's not greater than 240. Last Keys are the only
|
||||
# Magic Item wherein having more than one is relevant.
|
||||
else:
|
||||
# Decrease the Magic Item array offset by 1 if it's higher than the unused Map's item value.
|
||||
if array_offset > 5:
|
||||
array_offset -= 1
|
||||
if magic_items_array[array_offset] < 240:
|
||||
magic_items_array[array_offset] += 1
|
||||
|
||||
# Add the start inventory arrays to the offset data in bytes form.
|
||||
start_inventory_data[0x680080] = bytes(magic_items_array)
|
||||
start_inventory_data[0x6800A0] = bytes(cards_array)
|
||||
|
||||
# Add the extra max HP/MP/Hearts to all classes' base stats. Doing it this way makes us less likely to hit the max
|
||||
# possible Max Ups.
|
||||
# Vampire Killer
|
||||
start_inventory_data[0xE08C6] = int.to_bytes(100 + extra_stats["extra health"], 2, "little")
|
||||
start_inventory_data[0xE08CE] = int.to_bytes(100 + extra_stats["extra magic"], 2, "little")
|
||||
start_inventory_data[0xE08D4] = int.to_bytes(50 + extra_stats["extra hearts"], 2, "little")
|
||||
|
||||
# Magician
|
||||
start_inventory_data[0xE090E] = int.to_bytes(50 + extra_stats["extra health"], 2, "little")
|
||||
start_inventory_data[0xE0916] = int.to_bytes(400 + extra_stats["extra magic"], 2, "little")
|
||||
start_inventory_data[0xE091C] = int.to_bytes(50 + extra_stats["extra hearts"], 2, "little")
|
||||
|
||||
# Fighter
|
||||
start_inventory_data[0xE0932] = int.to_bytes(200 + extra_stats["extra health"], 2, "little")
|
||||
start_inventory_data[0xE093A] = int.to_bytes(50 + extra_stats["extra magic"], 2, "little")
|
||||
start_inventory_data[0xE0940] = int.to_bytes(50 + extra_stats["extra hearts"], 2, "little")
|
||||
|
||||
# Shooter
|
||||
start_inventory_data[0xE0832] = int.to_bytes(50 + extra_stats["extra health"], 2, "little")
|
||||
start_inventory_data[0xE08F2] = int.to_bytes(100 + extra_stats["extra magic"], 2, "little")
|
||||
start_inventory_data[0xE08F8] = int.to_bytes(250 + extra_stats["extra hearts"], 2, "little")
|
||||
|
||||
# Thief
|
||||
start_inventory_data[0xE0956] = int.to_bytes(50 + extra_stats["extra health"], 2, "little")
|
||||
start_inventory_data[0xE095E] = int.to_bytes(50 + extra_stats["extra magic"], 2, "little")
|
||||
start_inventory_data[0xE0964] = int.to_bytes(50 + extra_stats["extra hearts"], 2, "little")
|
||||
|
||||
return start_inventory_data, start_with_detonator
|
||||
563
worlds/cvcotm/client.py
Normal file
563
worlds/cvcotm/client.py
Normal file
@@ -0,0 +1,563 @@
|
||||
from typing import TYPE_CHECKING, Set
|
||||
from .locations import BASE_ID, get_location_names_to_ids
|
||||
from .items import cvcotm_item_info, MAJORS_CLASSIFICATIONS
|
||||
from .locations import cvcotm_location_info
|
||||
from .cvcotm_text import cvcotm_string_to_bytearray
|
||||
from .options import CompletionGoal, CVCotMDeathLink, IronMaidenBehavior
|
||||
from .rom import ARCHIPELAGO_IDENTIFIER_START, ARCHIPELAGO_IDENTIFIER, AUTH_NUMBER_START, QUEUED_TEXT_STRING_START
|
||||
from .data import iname, lname
|
||||
|
||||
from BaseClasses import ItemClassification
|
||||
from NetUtils import ClientStatus
|
||||
import worlds._bizhawk as bizhawk
|
||||
import base64
|
||||
from worlds._bizhawk.client import BizHawkClient
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from worlds._bizhawk.context import BizHawkClientContext
|
||||
|
||||
CURRENT_STATUS_ADDRESS = 0xD0
|
||||
POISON_TIMER_TILL_DAMAGE_ADDRESS = 0xD8
|
||||
POISON_DAMAGE_VALUE_ADDRESS = 0xDE
|
||||
GAME_STATE_ADDRESS = 0x45D8
|
||||
FLAGS_ARRAY_START = 0x25374
|
||||
CARDS_ARRAY_START = 0x25674
|
||||
NUM_RECEIVED_ITEMS_ADDRESS = 0x253D0
|
||||
MAX_UPS_ARRAY_START = 0x2572C
|
||||
MAGIC_ITEMS_ARRAY_START = 0x2572F
|
||||
QUEUED_TEXTBOX_1_ADDRESS = 0x25300
|
||||
QUEUED_TEXTBOX_2_ADDRESS = 0x25302
|
||||
QUEUED_MSG_DELAY_TIMER_ADDRESS = 0x25304
|
||||
QUEUED_SOUND_ID_ADDRESS = 0x25306
|
||||
DELAY_TIMER_ADDRESS = 0x25308
|
||||
CURRENT_CUTSCENE_ID_ADDRESS = 0x26000
|
||||
NATHAN_STATE_ADDRESS = 0x50
|
||||
CURRENT_HP_ADDRESS = 0x2562E
|
||||
CURRENT_MP_ADDRESS = 0x25636
|
||||
CURRENT_HEARTS_ADDRESS = 0x2563C
|
||||
CURRENT_LOCATION_VALUES_START = 0x253FC
|
||||
ROM_NAME_START = 0xA0
|
||||
|
||||
AREA_SEALED_ROOM = 0x00
|
||||
AREA_BATTLE_ARENA = 0x0E
|
||||
GAME_STATE_GAMEPLAY = 0x06
|
||||
GAME_STATE_CREDITS = 0x21
|
||||
NATHAN_STATE_SAVING = 0x34
|
||||
STATUS_POISON = b"\x02"
|
||||
TEXT_ID_DSS_TUTORIAL = b"\x1D\x82"
|
||||
TEXT_ID_MULTIWORLD_MESSAGE = b"\xF2\x84"
|
||||
SOUND_ID_UNUSED_SIMON_FANFARE = b"\x04"
|
||||
SOUND_ID_MAIDEN_BREAKING = b"\x79"
|
||||
# SOUND_ID_NATHAN_FREEZING = b"\x7A"
|
||||
SOUND_ID_BAD_CONFIG = b"\x2D\x01"
|
||||
SOUND_ID_DRACULA_CHARGE = b"\xAB\x01"
|
||||
SOUND_ID_MINOR_PICKUP = b"\xB3\x01"
|
||||
SOUND_ID_MAJOR_PICKUP = b"\xB4\x01"
|
||||
|
||||
ITEM_NAME_LIMIT = 300
|
||||
PLAYER_NAME_LIMIT = 50
|
||||
|
||||
FLAG_HIT_IRON_MAIDEN_SWITCH = 0x2A
|
||||
FLAG_SAW_DSS_TUTORIAL = 0xB1
|
||||
FLAG_WON_BATTLE_ARENA = 0xB2
|
||||
FLAG_DEFEATED_DRACULA_II = 0xBC
|
||||
|
||||
# These flags are communicated to the tracker as a bitfield using this order.
|
||||
# Modifying the order will cause undetectable autotracking issues.
|
||||
EVENT_FLAG_MAP = {
|
||||
FLAG_HIT_IRON_MAIDEN_SWITCH: "FLAG_HIT_IRON_MAIDEN_SWITCH",
|
||||
FLAG_WON_BATTLE_ARENA: "FLAG_WON_BATTLE_ARENA",
|
||||
0xB3: "FLAG_DEFEATED_CERBERUS",
|
||||
0xB4: "FLAG_DEFEATED_NECROMANCER",
|
||||
0xB5: "FLAG_DEFEATED_IRON_GOLEM",
|
||||
0xB6: "FLAG_DEFEATED_ADRAMELECH",
|
||||
0xB7: "FLAG_DEFEATED_DRAGON_ZOMBIES",
|
||||
0xB8: "FLAG_DEFEATED_DEATH",
|
||||
0xB9: "FLAG_DEFEATED_CAMILLA",
|
||||
0xBA: "FLAG_DEFEATED_HUGH",
|
||||
0xBB: "FLAG_DEFEATED_DRACULA_I",
|
||||
FLAG_DEFEATED_DRACULA_II: "FLAG_DEFEATED_DRACULA_II"
|
||||
}
|
||||
|
||||
DEATHLINK_AREA_NAMES = ["Sealed Room", "Catacomb", "Abyss Staircase", "Audience Room", "Triumph Hallway",
|
||||
"Machine Tower", "Eternal Corridor", "Chapel Tower", "Underground Warehouse",
|
||||
"Underground Gallery", "Underground Waterway", "Outer Wall", "Observation Tower",
|
||||
"Ceremonial Room", "Battle Arena"]
|
||||
|
||||
|
||||
class CastlevaniaCotMClient(BizHawkClient):
|
||||
game = "Castlevania - Circle of the Moon"
|
||||
system = "GBA"
|
||||
patch_suffix = ".apcvcotm"
|
||||
sent_initial_packets: bool
|
||||
self_induced_death: bool
|
||||
local_checked_locations: Set[int]
|
||||
client_set_events = {flag_name: False for flag, flag_name in EVENT_FLAG_MAP.items()}
|
||||
killed_dracula_2: bool
|
||||
won_battle_arena: bool
|
||||
sent_message_queue: list
|
||||
death_causes: list
|
||||
currently_dead: bool
|
||||
synced_set_events: bool
|
||||
saw_arena_win_message: bool
|
||||
saw_dss_tutorial: bool
|
||||
|
||||
async def validate_rom(self, ctx: "BizHawkClientContext") -> bool:
|
||||
from CommonClient import logger
|
||||
|
||||
try:
|
||||
# Check ROM name/patch version
|
||||
game_names = await bizhawk.read(ctx.bizhawk_ctx, [(ROM_NAME_START, 0xC, "ROM"),
|
||||
(ARCHIPELAGO_IDENTIFIER_START, 12, "ROM")])
|
||||
if game_names[0].decode("ascii") != "DRACULA AGB1":
|
||||
return False
|
||||
if game_names[1] == b'\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00\x00':
|
||||
logger.info("ERROR: You appear to be running an unpatched version of Castlevania: Circle of the Moon. "
|
||||
"You need to generate a patch file and use it to create a patched ROM.")
|
||||
return False
|
||||
if game_names[1].decode("ascii") != ARCHIPELAGO_IDENTIFIER:
|
||||
logger.info("ERROR: The patch file used to create this ROM is not compatible with "
|
||||
"this client. Double check your client version against the version being "
|
||||
"used by the generator.")
|
||||
return False
|
||||
except UnicodeDecodeError:
|
||||
return False
|
||||
except bizhawk.RequestFailedError:
|
||||
return False # Should verify on the next pass
|
||||
|
||||
ctx.game = self.game
|
||||
ctx.items_handling = 0b001
|
||||
ctx.want_slot_data = True
|
||||
ctx.watcher_timeout = 0.125
|
||||
return True
|
||||
|
||||
async def set_auth(self, ctx: "BizHawkClientContext") -> None:
|
||||
auth_raw = (await bizhawk.read(ctx.bizhawk_ctx, [(AUTH_NUMBER_START, 16, "ROM")]))[0]
|
||||
ctx.auth = base64.b64encode(auth_raw).decode("utf-8")
|
||||
# Initialize all the local client attributes here so that nothing will be carried over from a previous CotM if
|
||||
# the player tried changing CotM ROMs without resetting their Bizhawk Client instance.
|
||||
self.sent_initial_packets = False
|
||||
self.local_checked_locations = set()
|
||||
self.self_induced_death = False
|
||||
self.client_set_events = {flag_name: False for flag, flag_name in EVENT_FLAG_MAP.items()}
|
||||
self.killed_dracula_2 = False
|
||||
self.won_battle_arena = False
|
||||
self.sent_message_queue = []
|
||||
self.death_causes = []
|
||||
self.currently_dead = False
|
||||
self.synced_set_events = False
|
||||
self.saw_arena_win_message = False
|
||||
self.saw_dss_tutorial = False
|
||||
|
||||
def on_package(self, ctx: "BizHawkClientContext", cmd: str, args: dict) -> None:
|
||||
if cmd != "Bounced":
|
||||
return
|
||||
if "tags" not in args:
|
||||
return
|
||||
if ctx.slot is None:
|
||||
return
|
||||
if "DeathLink" in args["tags"] and args["data"]["source"] != ctx.slot_info[ctx.slot].name:
|
||||
if "cause" in args["data"]:
|
||||
cause = args["data"]["cause"]
|
||||
if cause == "":
|
||||
cause = f"{args['data']['source']} killed you without a word!"
|
||||
if len(cause) > ITEM_NAME_LIMIT + PLAYER_NAME_LIMIT:
|
||||
cause = cause[:ITEM_NAME_LIMIT + PLAYER_NAME_LIMIT]
|
||||
else:
|
||||
cause = f"{args['data']['source']} killed you without a word!"
|
||||
|
||||
# Highlight the player that killed us in the game's orange text.
|
||||
if args['data']['source'] in cause:
|
||||
words = cause.split(args['data']['source'], 1)
|
||||
cause = words[0] + "「" + args['data']['source'] + "」" + words[1]
|
||||
|
||||
self.death_causes += [cause]
|
||||
|
||||
async def game_watcher(self, ctx: "BizHawkClientContext") -> None:
|
||||
if ctx.server is None or ctx.server.socket.closed or ctx.slot_data is None or ctx.slot is None:
|
||||
return
|
||||
|
||||
try:
|
||||
# Scout all Locations and get our Set events upon initial connection.
|
||||
if not self.sent_initial_packets:
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "LocationScouts",
|
||||
"locations": [code for name, code in get_location_names_to_ids().items()
|
||||
if code in ctx.server_locations],
|
||||
"create_as_hint": 0
|
||||
}])
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "Get",
|
||||
"keys": [f"castlevania_cotm_events_{ctx.team}_{ctx.slot}"]
|
||||
}])
|
||||
self.sent_initial_packets = True
|
||||
|
||||
read_state = await bizhawk.read(ctx.bizhawk_ctx, [(GAME_STATE_ADDRESS, 1, "EWRAM"),
|
||||
(FLAGS_ARRAY_START, 32, "EWRAM"),
|
||||
(CARDS_ARRAY_START, 20, "EWRAM"),
|
||||
(NUM_RECEIVED_ITEMS_ADDRESS, 2, "EWRAM"),
|
||||
(MAX_UPS_ARRAY_START, 3, "EWRAM"),
|
||||
(MAGIC_ITEMS_ARRAY_START, 8, "EWRAM"),
|
||||
(QUEUED_TEXTBOX_1_ADDRESS, 2, "EWRAM"),
|
||||
(DELAY_TIMER_ADDRESS, 2, "EWRAM"),
|
||||
(CURRENT_CUTSCENE_ID_ADDRESS, 1, "EWRAM"),
|
||||
(NATHAN_STATE_ADDRESS, 1, "EWRAM"),
|
||||
(CURRENT_HP_ADDRESS, 18, "EWRAM"),
|
||||
(CURRENT_LOCATION_VALUES_START, 2, "EWRAM")])
|
||||
|
||||
game_state = int.from_bytes(read_state[0], "little")
|
||||
event_flags_array = read_state[1]
|
||||
cards_array = list(read_state[2])
|
||||
max_ups_array = list(read_state[4])
|
||||
magic_items_array = list(read_state[5])
|
||||
num_received_items = int.from_bytes(bytearray(read_state[3]), "little")
|
||||
queued_textbox = int.from_bytes(bytearray(read_state[6]), "little")
|
||||
delay_timer = int.from_bytes(bytearray(read_state[7]), "little")
|
||||
cutscene = int.from_bytes(bytearray(read_state[8]), "little")
|
||||
nathan_state = int.from_bytes(bytearray(read_state[9]), "little")
|
||||
health_stats_array = bytearray(read_state[10])
|
||||
area = int.from_bytes(bytearray(read_state[11][0:1]), "little")
|
||||
room = int.from_bytes(bytearray(read_state[11][1:]), "little")
|
||||
|
||||
# Get out each of the individual health/magic/heart values.
|
||||
hp = int.from_bytes(health_stats_array[0:2], "little")
|
||||
max_hp = int.from_bytes(health_stats_array[4:6], "little")
|
||||
# mp = int.from_bytes(health_stats_array[8:10], "little") Not used. But it's here if it's ever needed!
|
||||
max_mp = int.from_bytes(health_stats_array[12:14], "little")
|
||||
hearts = int.from_bytes(health_stats_array[14:16], "little")
|
||||
max_hearts = int.from_bytes(health_stats_array[16:], "little")
|
||||
|
||||
# If there's no textbox already queued, the delay timer is 0, we are not in a cutscene, and Nathan's current
|
||||
# state value is not 0x34 (using a save room), it should be safe to inject a textbox message.
|
||||
ok_to_inject = not queued_textbox and not delay_timer and not cutscene \
|
||||
and nathan_state != NATHAN_STATE_SAVING
|
||||
|
||||
# Make sure we are in the Gameplay or Credits states before detecting sent locations.
|
||||
# If we are in any other state, such as the Game Over state, reset the textbox buffers back to 0 so that we
|
||||
# don't receive the most recent item upon loading back in.
|
||||
#
|
||||
# If the intro cutscene floor broken flag is not set, then assume we are in the demo; at no point during
|
||||
# regular gameplay will this flag not be set.
|
||||
if game_state not in [GAME_STATE_GAMEPLAY, GAME_STATE_CREDITS] or not event_flags_array[6] & 0x02:
|
||||
self.currently_dead = False
|
||||
await bizhawk.write(ctx.bizhawk_ctx, [(QUEUED_TEXTBOX_1_ADDRESS, [0 for _ in range(12)], "EWRAM")])
|
||||
return
|
||||
|
||||
# Enable DeathLink if it's in our slot_data.
|
||||
if "DeathLink" not in ctx.tags and ctx.slot_data["death_link"]:
|
||||
await ctx.update_death_link(True)
|
||||
|
||||
# Send a DeathLink if we died on our own independently of receiving another one.
|
||||
if "DeathLink" in ctx.tags and hp == 0 and not self.currently_dead:
|
||||
self.currently_dead = True
|
||||
|
||||
# Check if we are in Dracula II's arena. The game considers this part of the Sealed Room area,
|
||||
# which I don't think makes sense to be player-facing like this.
|
||||
if area == AREA_SEALED_ROOM and room == 2:
|
||||
area_of_death = "Dracula's realm"
|
||||
# If we aren't in Dracula II's arena, then take the name of whatever area the player is currently in.
|
||||
else:
|
||||
area_of_death = DEATHLINK_AREA_NAMES[area]
|
||||
|
||||
await ctx.send_death(f"{ctx.player_names[ctx.slot]} perished in {area_of_death}. Dracula has won!")
|
||||
|
||||
# Update the Dracula II and Battle Arena events already being done on past separate sessions for if the
|
||||
# player is running the Battle Arena and Dracula goal.
|
||||
if f"castlevania_cotm_events_{ctx.team}_{ctx.slot}" in ctx.stored_data:
|
||||
if ctx.stored_data[f"castlevania_cotm_events_{ctx.team}_{ctx.slot}"] is not None:
|
||||
if ctx.stored_data[f"castlevania_cotm_events_{ctx.team}_{ctx.slot}"] & 0x2:
|
||||
self.won_battle_arena = True
|
||||
|
||||
if ctx.stored_data[f"castlevania_cotm_events_{ctx.team}_{ctx.slot}"] & 0x800:
|
||||
self.killed_dracula_2 = True
|
||||
|
||||
# If we won the Battle Arena, haven't seen the win message yet, and are in the Arena at the moment, pop up
|
||||
# the win message while playing the game's unused Theme of Simon Belmont fanfare.
|
||||
if self.won_battle_arena and not self.saw_arena_win_message and area == AREA_BATTLE_ARENA \
|
||||
and ok_to_inject and not self.currently_dead:
|
||||
win_message = cvcotm_string_to_bytearray(" A 「WINNER」 IS 「YOU」!▶", "little middle", 0,
|
||||
wrap=False)
|
||||
await bizhawk.write(ctx.bizhawk_ctx, [(QUEUED_TEXTBOX_1_ADDRESS, TEXT_ID_MULTIWORLD_MESSAGE, "EWRAM"),
|
||||
(QUEUED_SOUND_ID_ADDRESS, SOUND_ID_UNUSED_SIMON_FANFARE, "EWRAM"),
|
||||
(QUEUED_TEXT_STRING_START, win_message, "ROM")])
|
||||
self.saw_arena_win_message = True
|
||||
|
||||
# If we have any queued death causes, handle DeathLink giving here.
|
||||
elif self.death_causes and ok_to_inject and not self.currently_dead:
|
||||
|
||||
# Inject the oldest cause as a textbox message and play the Dracula charge attack sound.
|
||||
death_text = self.death_causes[0]
|
||||
death_writes = [(QUEUED_TEXTBOX_1_ADDRESS, TEXT_ID_MULTIWORLD_MESSAGE, "EWRAM"),
|
||||
(QUEUED_SOUND_ID_ADDRESS, SOUND_ID_DRACULA_CHARGE, "EWRAM")]
|
||||
|
||||
# If we are in the Battle Arena and are not using the On Including Arena DeathLink option, extend the
|
||||
# DeathLink message and don't actually kill Nathan.
|
||||
if ctx.slot_data["death_link"] != CVCotMDeathLink.option_arena_on and area == AREA_BATTLE_ARENA:
|
||||
death_text += "◊The Battle Arena nullified the DeathLink. Go fight fair and square!"
|
||||
else:
|
||||
# Otherwise, kill Nathan by giving him a 9999 damage-dealing poison status that hurts him as soon as
|
||||
# the death cause textbox is dismissed.
|
||||
death_writes += [(CURRENT_STATUS_ADDRESS, STATUS_POISON, "EWRAM"),
|
||||
(POISON_TIMER_TILL_DAMAGE_ADDRESS, b"\x38", "EWRAM"),
|
||||
(POISON_DAMAGE_VALUE_ADDRESS, b"\x0F\x27", "EWRAM")]
|
||||
|
||||
# Add the final death text and write the whole shebang.
|
||||
death_writes += [(QUEUED_TEXT_STRING_START,
|
||||
bytes(cvcotm_string_to_bytearray(death_text + "◊", "big middle", 0)), "ROM")]
|
||||
await bizhawk.write(ctx.bizhawk_ctx, death_writes)
|
||||
|
||||
# Delete the oldest death cause that we just wrote and set currently_dead to True so the client doesn't
|
||||
# think we just died on our own on the subsequent frames before the Game Over state.
|
||||
del(self.death_causes[0])
|
||||
self.currently_dead = True
|
||||
|
||||
# If we have a queue of Locations to inject "sent" messages with, do so before giving any subsequent Items.
|
||||
elif self.sent_message_queue and ok_to_inject and not self.currently_dead and ctx.locations_info:
|
||||
loc = self.sent_message_queue[0]
|
||||
# Truncate the Item name. ArchipIDLE's FFXIV Item is 214 characters, for comparison.
|
||||
item_name = ctx.item_names.lookup_in_slot(ctx.locations_info[loc].item, ctx.locations_info[loc].player)
|
||||
if len(item_name) > ITEM_NAME_LIMIT:
|
||||
item_name = item_name[:ITEM_NAME_LIMIT]
|
||||
# Truncate the player name. Player names are normally capped at 16 characters, but there is no limit on
|
||||
# ItemLink group names.
|
||||
player_name = ctx.player_names[ctx.locations_info[loc].player]
|
||||
if len(player_name) > PLAYER_NAME_LIMIT:
|
||||
player_name = player_name[:PLAYER_NAME_LIMIT]
|
||||
|
||||
sent_text = cvcotm_string_to_bytearray(f"「{item_name}」 sent to 「{player_name}」◊", "big middle", 0)
|
||||
|
||||
# Set the correct sound to play depending on the Item's classification.
|
||||
if item_name == iname.ironmaidens and \
|
||||
ctx.slot_info[ctx.locations_info[loc].player].game == "Castlevania - Circle of the Moon":
|
||||
mssg_sfx_id = SOUND_ID_MAIDEN_BREAKING
|
||||
sent_text = cvcotm_string_to_bytearray(f"「Iron Maidens」 broken for 「{player_name}」◊",
|
||||
"big middle", 0)
|
||||
elif ctx.locations_info[loc].flags & MAJORS_CLASSIFICATIONS:
|
||||
mssg_sfx_id = SOUND_ID_MAJOR_PICKUP
|
||||
elif ctx.locations_info[loc].flags & ItemClassification.trap:
|
||||
mssg_sfx_id = SOUND_ID_BAD_CONFIG
|
||||
else: # Filler
|
||||
mssg_sfx_id = SOUND_ID_MINOR_PICKUP
|
||||
|
||||
await bizhawk.write(ctx.bizhawk_ctx, [(QUEUED_TEXTBOX_1_ADDRESS, TEXT_ID_MULTIWORLD_MESSAGE, "EWRAM"),
|
||||
(QUEUED_SOUND_ID_ADDRESS, mssg_sfx_id, "EWRAM"),
|
||||
(QUEUED_TEXT_STRING_START, sent_text, "ROM")])
|
||||
|
||||
del(self.sent_message_queue[0])
|
||||
|
||||
# If the game hasn't received all items yet, it's ok to inject, and the current number of received items
|
||||
# still matches what we read before, then write the next incoming item into the inventory and, separately,
|
||||
# the textbox ID to trigger the multiworld textbox, sound effect to play when the textbox opens, number to
|
||||
# increment the received items count by, and the text to go into the multiworld textbox. The game will then
|
||||
# do the rest when it's able to.
|
||||
elif num_received_items < len(ctx.items_received) and ok_to_inject and not self.currently_dead:
|
||||
next_item = ctx.items_received[num_received_items]
|
||||
|
||||
# Figure out what inventory array and offset from said array to increment based on what we are
|
||||
# receiving.
|
||||
flag_index = 0
|
||||
flag_array = b""
|
||||
inv_array = []
|
||||
inv_array_start = 0
|
||||
text_id_2 = b"\x00\x00"
|
||||
item_type = next_item.item & 0xFF00
|
||||
inv_array_index = next_item.item & 0xFF
|
||||
if item_type == 0xE600: # Card
|
||||
inv_array_start = CARDS_ARRAY_START
|
||||
inv_array = cards_array
|
||||
mssg_sfx_id = SOUND_ID_MAJOR_PICKUP
|
||||
# If skip_tutorials is off and the saw DSS tutorial flag is not set, set the flag and display it
|
||||
# for the second textbox.
|
||||
if not self.saw_dss_tutorial and not ctx.slot_data["skip_tutorials"]:
|
||||
flag_index = FLAG_SAW_DSS_TUTORIAL
|
||||
flag_array = event_flags_array
|
||||
text_id_2 = TEXT_ID_DSS_TUTORIAL
|
||||
elif item_type == 0xE800 and inv_array_index == 0x09: # Maiden Detonator
|
||||
flag_index = FLAG_HIT_IRON_MAIDEN_SWITCH
|
||||
flag_array = event_flags_array
|
||||
mssg_sfx_id = SOUND_ID_MAIDEN_BREAKING
|
||||
elif item_type == 0xE800: # Any other Magic Item
|
||||
inv_array_start = MAGIC_ITEMS_ARRAY_START
|
||||
inv_array = magic_items_array
|
||||
mssg_sfx_id = SOUND_ID_MAJOR_PICKUP
|
||||
if inv_array_index > 5: # The unused Map's index is skipped over.
|
||||
inv_array_index -= 1
|
||||
else: # Max Up
|
||||
inv_array_start = MAX_UPS_ARRAY_START
|
||||
mssg_sfx_id = SOUND_ID_MINOR_PICKUP
|
||||
inv_array = max_ups_array
|
||||
|
||||
item_name = ctx.item_names.lookup_in_slot(next_item.item)
|
||||
player_name = ctx.player_names[next_item.player]
|
||||
# Truncate the player name.
|
||||
if len(player_name) > PLAYER_NAME_LIMIT:
|
||||
player_name = player_name[:PLAYER_NAME_LIMIT]
|
||||
|
||||
# If the Item came from a different player, display a custom received message. Otherwise, display the
|
||||
# vanilla received message for that Item.
|
||||
if next_item.player != ctx.slot:
|
||||
text_id_1 = TEXT_ID_MULTIWORLD_MESSAGE
|
||||
if item_name == iname.ironmaidens:
|
||||
received_text = cvcotm_string_to_bytearray(f"「Iron Maidens」 broken by "
|
||||
f"「{player_name}」◊", "big middle", 0)
|
||||
else:
|
||||
received_text = cvcotm_string_to_bytearray(f"「{item_name}」 received from "
|
||||
f"「{player_name}」◊", "big middle", 0)
|
||||
text_write = [(QUEUED_TEXT_STRING_START, bytes(received_text), "ROM")]
|
||||
|
||||
# If skip_tutorials is off, display the Item's tutorial for the second textbox (if it has one).
|
||||
if not ctx.slot_data["skip_tutorials"] and cvcotm_item_info[item_name].tutorial_id is not None:
|
||||
text_id_2 = cvcotm_item_info[item_name].tutorial_id
|
||||
else:
|
||||
text_id_1 = cvcotm_item_info[item_name].text_id
|
||||
text_write = []
|
||||
|
||||
# Check if the player has 255 of the item being received. If they do, don't increment that counter
|
||||
# further.
|
||||
refill_write = []
|
||||
count_write = []
|
||||
flag_write = []
|
||||
count_guard = []
|
||||
flag_guard = []
|
||||
|
||||
# If there's a value to increment in an inventory array, do so here after checking to see if we can.
|
||||
if inv_array_start:
|
||||
if inv_array[inv_array_index] + 1 > 0xFF:
|
||||
# If it's a stat max up being received, manually give a refill of that item's stat.
|
||||
# Normally, the game does this automatically by incrementing the number of that max up.
|
||||
if item_name == iname.hp_max:
|
||||
refill_write = [(CURRENT_HP_ADDRESS, int.to_bytes(max_hp, 2, "little"), "EWRAM")]
|
||||
elif item_name == iname.mp_max:
|
||||
refill_write = [(CURRENT_MP_ADDRESS, int.to_bytes(max_mp, 2, "little"), "EWRAM")]
|
||||
elif item_name == iname.heart_max:
|
||||
# If adding +6 Hearts doesn't put us over the player's current max Hearts, do so.
|
||||
# Otherwise, set the player's current Hearts to the current max.
|
||||
if hearts + 6 > max_hearts:
|
||||
new_hearts = max_hearts
|
||||
else:
|
||||
new_hearts = hearts + 6
|
||||
refill_write = [(CURRENT_HEARTS_ADDRESS, int.to_bytes(new_hearts, 2, "little"), "EWRAM")]
|
||||
else:
|
||||
# If our received count of that item is not more than 255, increment it normally.
|
||||
inv_address = inv_array_start + inv_array_index
|
||||
count_guard = [(inv_address, int.to_bytes(inv_array[inv_array_index], 1, "little"), "EWRAM")]
|
||||
count_write = [(inv_address, int.to_bytes(inv_array[inv_array_index] + 1, 1, "little"),
|
||||
"EWRAM")]
|
||||
|
||||
# If there's a flag value to set, do so here.
|
||||
if flag_index:
|
||||
flag_bytearray_index = flag_index // 8
|
||||
flag_address = FLAGS_ARRAY_START + flag_bytearray_index
|
||||
flag_guard = [(flag_address, int.to_bytes(flag_array[flag_bytearray_index], 1, "little"), "EWRAM")]
|
||||
flag_write = [(flag_address, int.to_bytes(flag_array[flag_bytearray_index] |
|
||||
(0x01 << (flag_index % 8)), 1, "little"), "EWRAM")]
|
||||
|
||||
await bizhawk.guarded_write(ctx.bizhawk_ctx,
|
||||
[(QUEUED_TEXTBOX_1_ADDRESS, text_id_1, "EWRAM"),
|
||||
(QUEUED_TEXTBOX_2_ADDRESS, text_id_2, "EWRAM"),
|
||||
(QUEUED_MSG_DELAY_TIMER_ADDRESS, b"\x01", "EWRAM"),
|
||||
(QUEUED_SOUND_ID_ADDRESS, mssg_sfx_id, "EWRAM")]
|
||||
+ count_write + flag_write + text_write + refill_write,
|
||||
# Make sure the number of received items and number to overwrite are still
|
||||
# what we expect them to be.
|
||||
[(NUM_RECEIVED_ITEMS_ADDRESS, read_state[3], "EWRAM")]
|
||||
+ count_guard + flag_guard),
|
||||
|
||||
locs_to_send = set()
|
||||
|
||||
# Check each bit in each flag byte for set Location and event flags.
|
||||
checked_set_events = {flag_name: False for flag, flag_name in EVENT_FLAG_MAP.items()}
|
||||
for byte_index, byte in enumerate(event_flags_array):
|
||||
for i in range(8):
|
||||
and_value = 0x01 << i
|
||||
if byte & and_value != 0:
|
||||
flag_id = byte_index * 8 + i
|
||||
|
||||
location_id = flag_id + BASE_ID
|
||||
if location_id in ctx.server_locations:
|
||||
locs_to_send.add(location_id)
|
||||
|
||||
# If the flag for pressing the Iron Maiden switch is set, and the Iron Maiden behavior is
|
||||
# vanilla (meaning we really pressed the switch), send the Iron Maiden switch as checked.
|
||||
if flag_id == FLAG_HIT_IRON_MAIDEN_SWITCH and ctx.slot_data["iron_maiden_behavior"] == \
|
||||
IronMaidenBehavior.option_vanilla:
|
||||
locs_to_send.add(cvcotm_location_info[lname.ct21].code + BASE_ID)
|
||||
|
||||
# If the DSS tutorial flag is set, let the client know, so it's not shown again for
|
||||
# subsequently-received cards.
|
||||
if flag_id == FLAG_SAW_DSS_TUTORIAL:
|
||||
self.saw_dss_tutorial = True
|
||||
|
||||
if flag_id in EVENT_FLAG_MAP:
|
||||
checked_set_events[EVENT_FLAG_MAP[flag_id]] = True
|
||||
|
||||
# Update the client's statuses for the Battle Arena and Dracula goals.
|
||||
if flag_id == FLAG_WON_BATTLE_ARENA:
|
||||
self.won_battle_arena = True
|
||||
|
||||
if flag_id == FLAG_DEFEATED_DRACULA_II:
|
||||
self.killed_dracula_2 = True
|
||||
|
||||
# Send Locations if there are any to send.
|
||||
if locs_to_send != self.local_checked_locations:
|
||||
self.local_checked_locations = locs_to_send
|
||||
|
||||
if locs_to_send is not None:
|
||||
# Capture all the Locations with non-local Items to send that are in ctx.missing_locations
|
||||
# (the ones that were definitely never sent before).
|
||||
if ctx.locations_info:
|
||||
self.sent_message_queue += [loc for loc in locs_to_send if loc in ctx.missing_locations and
|
||||
ctx.locations_info[loc].player != ctx.slot]
|
||||
# If we still don't have the locations info at this point, send another LocationScout packet just
|
||||
# in case something went wrong, and we never received the initial LocationInfo packet.
|
||||
else:
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "LocationScouts",
|
||||
"locations": [code for name, code in get_location_names_to_ids().items()
|
||||
if code in ctx.server_locations],
|
||||
"create_as_hint": 0
|
||||
}])
|
||||
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "LocationChecks",
|
||||
"locations": list(locs_to_send)
|
||||
}])
|
||||
|
||||
# Check the win condition depending on what our completion goal is.
|
||||
# The Dracula option requires the "killed Dracula II" flag to be set or being in the credits state.
|
||||
# The Battle Arena option requires the Shinning Armor pickup flag to be set.
|
||||
# Otherwise, the Battle Arena and Dracula option requires both of the above to be satisfied simultaneously.
|
||||
if ctx.slot_data["completion_goal"] == CompletionGoal.option_dracula:
|
||||
win_condition = self.killed_dracula_2
|
||||
elif ctx.slot_data["completion_goal"] == CompletionGoal.option_battle_arena:
|
||||
win_condition = self.won_battle_arena
|
||||
else:
|
||||
win_condition = self.killed_dracula_2 and self.won_battle_arena
|
||||
|
||||
# Send game clear if we've satisfied the win condition.
|
||||
if not ctx.finished_game and win_condition:
|
||||
ctx.finished_game = True
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "StatusUpdate",
|
||||
"status": ClientStatus.CLIENT_GOAL
|
||||
}])
|
||||
|
||||
# Update the tracker event flags
|
||||
if checked_set_events != self.client_set_events and ctx.slot is not None:
|
||||
event_bitfield = 0
|
||||
for index, (flag, flag_name) in enumerate(EVENT_FLAG_MAP.items()):
|
||||
if checked_set_events[flag_name]:
|
||||
event_bitfield |= 1 << index
|
||||
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "Set",
|
||||
"key": f"castlevania_cotm_events_{ctx.team}_{ctx.slot}",
|
||||
"default": 0,
|
||||
"want_reply": False,
|
||||
"operations": [{"operation": "or", "value": event_bitfield}],
|
||||
}])
|
||||
self.client_set_events = checked_set_events
|
||||
|
||||
except bizhawk.RequestFailedError:
|
||||
# Exit handler and return to main loop to reconnect.
|
||||
pass
|
||||
178
worlds/cvcotm/cvcotm_text.py
Normal file
178
worlds/cvcotm/cvcotm_text.py
Normal file
@@ -0,0 +1,178 @@
|
||||
from typing import Literal
|
||||
|
||||
cvcotm_char_dict = {"\n": 0x09, " ": 0x26, "!": 0x4A, '"': 0x78, "#": 0x79, "$": 0x7B, "%": 0x68, "&": 0x73, "'": 0x51,
|
||||
"(": 0x54, ")": 0x55, "*": 0x7A, "+": 0x50, ",": 0x4C, "-": 0x58, ".": 0x35, "/": 0x70, "0": 0x64,
|
||||
"1": 0x6A, "2": 0x63, "3": 0x6C, "4": 0x71, "5": 0x69, "6": 0x7C, "7": 0x7D, "8": 0x72, "9": 0x85,
|
||||
":": 0x86, ";": 0x87, "<": 0x8F, "=": 0x90, ">": 0x91, "?": 0x48, "@": 0x98, "A": 0x3E, "B": 0x4D,
|
||||
"C": 0x44, "D": 0x45, "E": 0x4E, "F": 0x56, "G": 0x4F, "H": 0x40, "I": 0x43, "J": 0x6B, "K": 0x66,
|
||||
"L": 0x5F, "M": 0x42, "N": 0x52, "O": 0x67, "P": 0x4B, "Q": 0x99, "R": 0x46, "S": 0x41, "T": 0x47,
|
||||
"U": 0x60, "V": 0x6E, "W": 0x49, "X": 0x6D, "Y": 0x53, "Z": 0x6F, "[": 0x59, "\\": 0x9A, "]": 0x5A,
|
||||
"^": 0x9B, "_": 0xA1, "a": 0x29, "b": 0x3C, "c": 0x33, "d": 0x32, "e": 0x28, "f": 0x3A, "g": 0x39,
|
||||
"h": 0x31, "i": 0x2D, "j": 0x62, "k": 0x3D, "l": 0x30, "m": 0x36, "n": 0x2E, "o": 0x2B, "p": 0x38,
|
||||
"q": 0x61, "r": 0x2C, "s": 0x2F, "t": 0x2A, "u": 0x34, "v": 0x3F, "w": 0x37, "x": 0x57, "y": 0x3B,
|
||||
"z": 0x65, "{": 0xA3, "|": 0xA4, "}": 0xA5, "`": 0xA2, "~": 0xAC,
|
||||
# Special command characters
|
||||
"▶": 0x02, # Press A with prompt arrow.
|
||||
"◊": 0x03, # Press A without prompt arrow.
|
||||
"\t": 0x01, # Clear the text buffer; usually after pressing A to advance.
|
||||
"\b": 0x0A, # Reset text alignment; usually after pressing A.
|
||||
"「": 0x06, # Start orange text
|
||||
"」": 0x07, # End orange text
|
||||
}
|
||||
|
||||
# Characters that do not contribute to the line length.
|
||||
weightless_chars = {"\n", "▶", "◊", "\b", "\t", "「", "」"}
|
||||
|
||||
|
||||
def cvcotm_string_to_bytearray(cvcotm_text: str, textbox_type: Literal["big top", "big middle", "little middle"],
|
||||
speed: int, portrait: int = 0xFF, wrap: bool = True,
|
||||
skip_textbox_controllers: bool = False) -> bytearray:
|
||||
"""Converts a string into a textbox bytearray following CVCotM's string format."""
|
||||
text_bytes = bytearray(0)
|
||||
if portrait == 0xFF and textbox_type != "little middle":
|
||||
text_bytes.append(0x0C) # Insert the character to convert a 3-line named textbox into a 4-line nameless one.
|
||||
|
||||
# Figure out the start and end params for the textbox based on what type it is.
|
||||
if textbox_type == "little middle":
|
||||
main_control_start_param = 0x10
|
||||
main_control_end_param = 0x20
|
||||
elif textbox_type == "big top":
|
||||
main_control_start_param = 0x40
|
||||
main_control_end_param = 0xC0
|
||||
else:
|
||||
main_control_start_param = 0x80
|
||||
main_control_end_param = 0xC0
|
||||
|
||||
# Figure out the number of lines and line length limit.
|
||||
if textbox_type == "little middle":
|
||||
total_lines = 1
|
||||
len_limit = 29
|
||||
elif textbox_type != "little middle" and portrait != 0xFF:
|
||||
total_lines = 3
|
||||
len_limit = 21
|
||||
else:
|
||||
total_lines = 4
|
||||
len_limit = 23
|
||||
|
||||
# Wrap the text if we are opting to do so.
|
||||
if wrap:
|
||||
refined_text = cvcotm_text_wrap(cvcotm_text, len_limit, total_lines)
|
||||
else:
|
||||
refined_text = cvcotm_text
|
||||
|
||||
# Add the textbox control characters if we are opting to add them.
|
||||
if not skip_textbox_controllers:
|
||||
text_bytes.extend([0x1D, main_control_start_param + (speed & 0xF)]) # Speed should be a value between 0 and 15.
|
||||
|
||||
# Add the portrait (if we are adding one).
|
||||
if portrait != 0xFF and textbox_type != "little middle":
|
||||
text_bytes.extend([0x1E, portrait & 0xFF])
|
||||
|
||||
for i, char in enumerate(refined_text):
|
||||
if char in cvcotm_char_dict:
|
||||
text_bytes.extend([cvcotm_char_dict[char]])
|
||||
# If we're pressing A to advance, add the text clear and reset alignment characters.
|
||||
if char in ["▶", "◊"] and not skip_textbox_controllers:
|
||||
text_bytes.extend([0x01, 0x0A])
|
||||
else:
|
||||
text_bytes.extend([0x48])
|
||||
|
||||
# Add the characters indicating the end of the whole message.
|
||||
if not skip_textbox_controllers:
|
||||
text_bytes.extend([0x1D, main_control_end_param, 0x00])
|
||||
else:
|
||||
text_bytes.extend([0x00])
|
||||
return text_bytes
|
||||
|
||||
|
||||
def cvcotm_text_truncate(cvcotm_text: str, textbox_len_limit: int) -> str:
|
||||
"""Truncates a string at a given in-game text line length."""
|
||||
line_len = 0
|
||||
|
||||
for i in range(len(cvcotm_text)):
|
||||
if cvcotm_text[i] not in weightless_chars:
|
||||
line_len += 1
|
||||
|
||||
if line_len > textbox_len_limit:
|
||||
return cvcotm_text[0x00:i]
|
||||
|
||||
return cvcotm_text
|
||||
|
||||
|
||||
def cvcotm_text_wrap(cvcotm_text: str, textbox_len_limit: int, total_lines: int = 4) -> str:
|
||||
"""Rebuilds a string with some of its spaces replaced with newlines to ensure the text wraps properly in an in-game
|
||||
textbox of a given length. If the number of lines allowed per textbox is exceeded, an A prompt will be placed
|
||||
instead of a newline."""
|
||||
words = cvcotm_text.split(" ")
|
||||
new_text = ""
|
||||
line_len = 0
|
||||
num_lines = 1
|
||||
|
||||
for word_index, word in enumerate(words):
|
||||
# Reset the word length to 0 on every word iteration and make its default divider a space.
|
||||
word_len = 0
|
||||
word_divider = " "
|
||||
|
||||
# Check if we're at the very beginning of a line and handle the situation accordingly by increasing the current
|
||||
# line length to account for the space if we are not. Otherwise, the word divider should be nothing.
|
||||
if line_len != 0:
|
||||
line_len += 1
|
||||
else:
|
||||
word_divider = ""
|
||||
|
||||
new_word = ""
|
||||
|
||||
for char_index, char in enumerate(word):
|
||||
# Check if the current character contributes to the line length.
|
||||
if char not in weightless_chars:
|
||||
line_len += 1
|
||||
word_len += 1
|
||||
|
||||
# If we're looking at a manually-placed newline, add +1 to the lines counter and reset the length counters.
|
||||
if char == "\n":
|
||||
word_len = 0
|
||||
line_len = 0
|
||||
num_lines += 1
|
||||
# If this puts us over the line limit, insert the A advance prompt character.
|
||||
if num_lines > total_lines:
|
||||
num_lines = 1
|
||||
new_word += "▶"
|
||||
|
||||
# If we're looking at a manually-placed A advance prompt, reset the lines and length counters.
|
||||
if char in ["▶", "◊"]:
|
||||
word_len = 0
|
||||
line_len = 0
|
||||
num_lines = 1
|
||||
|
||||
# If the word alone is long enough to exceed the line length, wrap without moving the entire word.
|
||||
if word_len > textbox_len_limit:
|
||||
word_len = 1
|
||||
line_len = 1
|
||||
num_lines += 1
|
||||
word_splitter = "\n"
|
||||
|
||||
# If this puts us over the line limit, replace the newline with the A advance prompt character.
|
||||
if num_lines > total_lines:
|
||||
num_lines = 1
|
||||
word_splitter = "▶"
|
||||
|
||||
new_word += word_splitter
|
||||
|
||||
# If the total length of the current line exceeds the line length, wrap the current word to the next line.
|
||||
if line_len > textbox_len_limit:
|
||||
word_divider = "\n"
|
||||
line_len = word_len
|
||||
num_lines += 1
|
||||
# If we're over the allowed number of lines to be displayed in the textbox, insert the A advance
|
||||
# character instead.
|
||||
if num_lines > total_lines:
|
||||
num_lines = 1
|
||||
word_divider = "▶"
|
||||
|
||||
# Add the character to the new word if the character is not a newline immediately following up an A advance.
|
||||
if char != "\n" or new_word[len(new_word)-1] not in ["▶", "◊"]:
|
||||
new_word += char
|
||||
|
||||
new_text += word_divider + new_word
|
||||
|
||||
return new_text
|
||||
36
worlds/cvcotm/data/iname.py
Normal file
36
worlds/cvcotm/data/iname.py
Normal file
@@ -0,0 +1,36 @@
|
||||
double = "Double"
|
||||
tackle = "Tackle"
|
||||
kick_boots = "Kick Boots"
|
||||
heavy_ring = "Heavy Ring"
|
||||
cleansing = "Cleansing"
|
||||
roc_wing = "Roc Wing"
|
||||
last_key = "Last Key"
|
||||
ironmaidens = "Maiden Detonator"
|
||||
|
||||
heart_max = "Heart Max Up"
|
||||
mp_max = "MP Max Up"
|
||||
hp_max = "HP Max Up"
|
||||
|
||||
salamander = "Salamander Card"
|
||||
serpent = "Serpent Card"
|
||||
mandragora = "Mandragora Card"
|
||||
golem = "Golem Card"
|
||||
cockatrice = "Cockatrice Card"
|
||||
manticore = "Manticore Card"
|
||||
griffin = "Griffin Card"
|
||||
thunderbird = "Thunderbird Card"
|
||||
unicorn = "Unicorn Card"
|
||||
black_dog = "Black Dog Card"
|
||||
mercury = "Mercury Card"
|
||||
venus = "Venus Card"
|
||||
jupiter = "Jupiter Card"
|
||||
mars = "Mars Card"
|
||||
diana = "Diana Card"
|
||||
apollo = "Apollo Card"
|
||||
neptune = "Neptune Card"
|
||||
saturn = "Saturn Card"
|
||||
uranus = "Uranus Card"
|
||||
pluto = "Pluto Card"
|
||||
|
||||
dracula = "The Count Downed"
|
||||
shinning_armor = "Where's My Super Suit?"
|
||||
BIN
worlds/cvcotm/data/ips/AllowAlwaysDrop.ips
Normal file
BIN
worlds/cvcotm/data/ips/AllowAlwaysDrop.ips
Normal file
Binary file not shown.
BIN
worlds/cvcotm/data/ips/AllowSpeedDash.ips
Normal file
BIN
worlds/cvcotm/data/ips/AllowSpeedDash.ips
Normal file
Binary file not shown.
BIN
worlds/cvcotm/data/ips/BrokenMaidens.ips
Normal file
BIN
worlds/cvcotm/data/ips/BrokenMaidens.ips
Normal file
Binary file not shown.
BIN
worlds/cvcotm/data/ips/BuffFamiliars.ips
Normal file
BIN
worlds/cvcotm/data/ips/BuffFamiliars.ips
Normal file
Binary file not shown.
BIN
worlds/cvcotm/data/ips/BuffSubweapons.ips
Normal file
BIN
worlds/cvcotm/data/ips/BuffSubweapons.ips
Normal file
Binary file not shown.
BIN
worlds/cvcotm/data/ips/CandleFix.ips
Normal file
BIN
worlds/cvcotm/data/ips/CandleFix.ips
Normal file
Binary file not shown.
BIN
worlds/cvcotm/data/ips/CardCombosRevealed.ips
Normal file
BIN
worlds/cvcotm/data/ips/CardCombosRevealed.ips
Normal file
Binary file not shown.
BIN
worlds/cvcotm/data/ips/CardUp_v3_Custom2.ips
Normal file
BIN
worlds/cvcotm/data/ips/CardUp_v3_Custom2.ips
Normal file
Binary file not shown.
BIN
worlds/cvcotm/data/ips/Countdown.ips
Normal file
BIN
worlds/cvcotm/data/ips/Countdown.ips
Normal file
Binary file not shown.
BIN
worlds/cvcotm/data/ips/DSSGlitchFix.ips
Normal file
BIN
worlds/cvcotm/data/ips/DSSGlitchFix.ips
Normal file
Binary file not shown.
BIN
worlds/cvcotm/data/ips/DSSRunSpeed.ips
Normal file
BIN
worlds/cvcotm/data/ips/DSSRunSpeed.ips
Normal file
Binary file not shown.
BIN
worlds/cvcotm/data/ips/DemoForceFirst.ips
Normal file
BIN
worlds/cvcotm/data/ips/DemoForceFirst.ips
Normal file
Binary file not shown.
BIN
worlds/cvcotm/data/ips/DropReworkMultiEdition.ips
Normal file
BIN
worlds/cvcotm/data/ips/DropReworkMultiEdition.ips
Normal file
Binary file not shown.
BIN
worlds/cvcotm/data/ips/GameClearBypass.ips
Normal file
BIN
worlds/cvcotm/data/ips/GameClearBypass.ips
Normal file
Binary file not shown.
BIN
worlds/cvcotm/data/ips/MPComboFix.ips
Normal file
BIN
worlds/cvcotm/data/ips/MPComboFix.ips
Normal file
Binary file not shown.
BIN
worlds/cvcotm/data/ips/MapEdits.ips
Normal file
BIN
worlds/cvcotm/data/ips/MapEdits.ips
Normal file
Binary file not shown.
BIN
worlds/cvcotm/data/ips/MultiLastKey.ips
Normal file
BIN
worlds/cvcotm/data/ips/MultiLastKey.ips
Normal file
Binary file not shown.
BIN
worlds/cvcotm/data/ips/NoDSSDrops.ips
Normal file
BIN
worlds/cvcotm/data/ips/NoDSSDrops.ips
Normal file
Binary file not shown.
BIN
worlds/cvcotm/data/ips/NoMPDrain.ips
Normal file
BIN
worlds/cvcotm/data/ips/NoMPDrain.ips
Normal file
Binary file not shown.
BIN
worlds/cvcotm/data/ips/PermanentDash.ips
Normal file
BIN
worlds/cvcotm/data/ips/PermanentDash.ips
Normal file
Binary file not shown.
BIN
worlds/cvcotm/data/ips/SeedDisplay20Digits.ips
Normal file
BIN
worlds/cvcotm/data/ips/SeedDisplay20Digits.ips
Normal file
Binary file not shown.
BIN
worlds/cvcotm/data/ips/ShooterStrength.ips
Normal file
BIN
worlds/cvcotm/data/ips/ShooterStrength.ips
Normal file
Binary file not shown.
BIN
worlds/cvcotm/data/ips/SoftlockBlockFix.ips
Normal file
BIN
worlds/cvcotm/data/ips/SoftlockBlockFix.ips
Normal file
Binary file not shown.
128
worlds/cvcotm/data/lname.py
Normal file
128
worlds/cvcotm/data/lname.py
Normal file
@@ -0,0 +1,128 @@
|
||||
sr3 = "Sealed Room: Main shaft left fake wall"
|
||||
cc1 = "Catacomb: Push crate treasure room"
|
||||
cc3 = "Catacomb: Fleamen brain room - Lower"
|
||||
cc3b = "Catacomb: Fleamen brain room - Upper"
|
||||
cc4 = "Catacomb: Earth Demon dash room"
|
||||
cc5 = "Catacomb: Tackle block treasure room"
|
||||
cc8 = "Catacomb: Earth Demon bone pit - Lower"
|
||||
cc8b = "Catacomb: Earth Demon bone pit - Upper"
|
||||
cc9 = "Catacomb: Below right column save room"
|
||||
cc10 = "Catacomb: Right column fake wall"
|
||||
cc13 = "Catacomb: Right column Spirit room"
|
||||
cc14 = "Catacomb: Muddy Mudman platforms room - Lower"
|
||||
cc14b = "Catacomb: Muddy Mudman platforms room - Upper"
|
||||
cc16 = "Catacomb: Slide space zone"
|
||||
cc20 = "Catacomb: Pre-Cerberus lone Skeleton room"
|
||||
cc22 = "Catacomb: Pre-Cerberus Hopper treasure room"
|
||||
cc24 = "Catacomb: Behind Cerberus"
|
||||
cc25 = "Catacomb: Mummies' fake wall"
|
||||
as2 = "Abyss Staircase: Lower fake wall"
|
||||
as3 = "Abyss Staircase: Loopback drop"
|
||||
as4 = "Abyss Staircase: Roc ledge"
|
||||
as9 = "Abyss Staircase: Upper fake wall"
|
||||
ar4 = "Audience Room: Skeleton foyer fake wall"
|
||||
ar7 = "Audience Room: Main gallery fake wall"
|
||||
ar8 = "Audience Room: Below coyote jump"
|
||||
ar9 = "Audience Room: Push crate gallery"
|
||||
ar10 = "Audience Room: Past coyote jump"
|
||||
ar11 = "Audience Room: Tackle block gallery"
|
||||
ar14 = "Audience Room: Wicked roc chamber - Lower"
|
||||
ar14b = "Audience Room: Wicked roc chamber - Upper"
|
||||
ar16 = "Audience Room: Upper Devil Tower hallway"
|
||||
ar17 = "Audience Room: Right exterior - Lower"
|
||||
ar17b = "Audience Room: Right exterior - Upper"
|
||||
ar18 = "Audience Room: Right exterior fake wall"
|
||||
ar19 = "Audience Room: 100 meter skelly dash hallway"
|
||||
ar21 = "Audience Room: Lower Devil Tower hallway fake wall"
|
||||
ar25 = "Audience Room: Behind Necromancer"
|
||||
ar26 = "Audience Room: Below Machine Tower roc ledge"
|
||||
ar27 = "Audience Room: Below Machine Tower push crate room"
|
||||
ar30 = "Audience Room: Roc horse jaguar armory - Left"
|
||||
ar30b = "Audience Room: Roc horse jaguar armory - Right"
|
||||
ow0 = "Outer Wall: Left roc ledge"
|
||||
ow1 = "Outer Wall: Right-brained ledge"
|
||||
ow2 = "Outer Wall: Fake Nightmare floor"
|
||||
th1 = "Triumph Hallway: Skeleton slopes fake wall"
|
||||
th3 = "Triumph Hallway: Entrance Flame Armor climb"
|
||||
mt0 = "Machine Tower: Foxy platforms ledge"
|
||||
mt2 = "Machine Tower: Knight fox meeting room"
|
||||
mt3 = "Machine Tower: Boneheaded argument wall kicks room"
|
||||
mt4 = "Machine Tower: Foxy fake wall"
|
||||
mt6 = "Machine Tower: Skelly-rang wall kicks room"
|
||||
mt8 = "Machine Tower: Fake Lilim wall"
|
||||
mt10 = "Machine Tower: Thunderous zone fake wall"
|
||||
mt11 = "Machine Tower: Thunderous zone lone Stone Armor room"
|
||||
mt13 = "Machine Tower: Top lone Stone Armor room"
|
||||
mt14 = "Machine Tower: Stone fox hallway"
|
||||
mt17 = "Machine Tower: Pre-Iron Golem fake wall"
|
||||
mt19 = "Machine Tower: Behind Iron Golem"
|
||||
ec5 = "Eternal Corridor: Midway fake wall"
|
||||
ec7 = "Eternal Corridor: Skelly-rang wall kicks room"
|
||||
ec9 = "Eternal Corridor: Skelly-rang fake wall"
|
||||
ct1 = "Chapel Tower: Flame Armor climb room"
|
||||
ct4 = "Chapel Tower: Lower chapel push crate room"
|
||||
ct5 = "Chapel Tower: Lower chapel fake wall"
|
||||
ct6 = "Chapel Tower: Beastly wall kicks room - Brain side"
|
||||
ct6b = "Chapel Tower: Beastly wall kicks room - Brawn side"
|
||||
ct8 = "Chapel Tower: Middle chapel fake wall"
|
||||
ct10 = "Chapel Tower: Middle chapel push crate room"
|
||||
ct13 = "Chapel Tower: Sharp mind climb room"
|
||||
ct15 = "Chapel Tower: Upper chapel fake wall"
|
||||
ct16 = "Chapel Tower: Upper chapel Marionette wall kicks"
|
||||
ct18 = "Chapel Tower: Upper belfry fake wall"
|
||||
ct21 = "Chapel Tower: Iron maiden switch"
|
||||
ct22 = "Chapel Tower: Behind Adramelech iron maiden"
|
||||
ct26 = "Chapel Tower: Outside Battle Arena - Upper"
|
||||
ct26b = "Chapel Tower: Outside Battle Arena - Lower"
|
||||
ug0 = "Underground Gallery: Conveyor platform ride"
|
||||
ug1 = "Underground Gallery: Conveyor upper push crate room"
|
||||
ug2 = "Underground Gallery: Conveyor lower push crate room"
|
||||
ug3 = "Underground Gallery: Harpy climb room - Lower"
|
||||
ug3b = "Underground Gallery: Harpy climb room - Upper"
|
||||
ug8 = "Underground Gallery: Harpy mantis tackle hallway"
|
||||
ug10 = "Underground Gallery: Handy bee hallway"
|
||||
ug13 = "Underground Gallery: Myconid fake wall"
|
||||
ug15 = "Underground Gallery: Crumble bridge fake wall"
|
||||
ug20 = "Underground Gallery: Behind Dragon Zombies"
|
||||
uw1 = "Underground Warehouse: Entrance push crate room"
|
||||
uw6 = "Underground Warehouse: Forever pushing room"
|
||||
uw8 = "Underground Warehouse: Crate-nudge fox room"
|
||||
uw9 = "Underground Warehouse: Crate-nudge fake wall"
|
||||
uw10 = "Underground Warehouse: Succubus shaft roc ledge"
|
||||
uw11 = "Underground Warehouse: Fake Lilith wall"
|
||||
uw14 = "Underground Warehouse: Optional puzzle ceiling fake wall"
|
||||
uw16 = "Underground Warehouse: Holy fox hideout - Left"
|
||||
uw16b = "Underground Warehouse: Holy fox hideout - Right roc ledge"
|
||||
uw19 = "Underground Warehouse: Forest Armor's domain fake wall"
|
||||
uw23 = "Underground Warehouse: Behind Death"
|
||||
uw24 = "Underground Warehouse: Behind Death fake wall"
|
||||
uw25 = "Underground Warehouse: Dryad expulsion chamber"
|
||||
uy1 = "Underground Waterway: Entrance fake wall"
|
||||
uy3 = "Underground Waterway: Before illusory wall"
|
||||
uy3b = "Underground Waterway: Beyond illusory wall"
|
||||
uy4 = "Underground Waterway: Ice Armor's domain fake wall"
|
||||
uy5 = "Underground Waterway: Brain freeze room"
|
||||
uy7 = "Underground Waterway: Middle lone Ice Armor room"
|
||||
uy8 = "Underground Waterway: Roc fake ceiling"
|
||||
uy9 = "Underground Waterway: Wicked Fishhead moat - Bottom"
|
||||
uy9b = "Underground Waterway: Wicked Fishhead moat - Top"
|
||||
uy12 = "Underground Waterway: Lizard-man turf - Bottom"
|
||||
uy12b = "Underground Waterway: Lizard-man turf - Top"
|
||||
uy13 = "Underground Waterway: Roc exit shaft"
|
||||
uy17 = "Underground Waterway: Behind Camilla"
|
||||
uy18 = "Underground Waterway: Roc exit shaft fake wall"
|
||||
ot1 = "Observation Tower: Wind Armor rampart"
|
||||
ot2 = "Observation Tower: Legion plaza fake wall"
|
||||
ot3 = "Observation Tower: Legion plaza Minotaur hallway"
|
||||
ot5 = "Observation Tower: Siren balcony fake wall"
|
||||
ot8 = "Observation Tower: Evil Pillar pit fake wall"
|
||||
ot9 = "Observation Tower: Alraune garden"
|
||||
ot12 = "Observation Tower: Dark Armor's domain fake wall"
|
||||
ot13 = "Observation Tower: Catoblepeas hallway"
|
||||
ot16 = "Observation Tower: Near warp room fake wall"
|
||||
ot20 = "Observation Tower: Behind Hugh"
|
||||
cr1 = "Ceremonial Room: Fake floor"
|
||||
ba24 = "Battle Arena: End reward"
|
||||
|
||||
arena_victory = "Arena Victory"
|
||||
dracula = "Dracula"
|
||||
431
worlds/cvcotm/data/patches.py
Normal file
431
worlds/cvcotm/data/patches.py
Normal file
@@ -0,0 +1,431 @@
|
||||
remote_textbox_shower = [
|
||||
# Pops up the textbox(s) of whatever textbox IDs is written at 0x02025300 and 0x02025302 and increments the current
|
||||
# received item index at 0x020253D0 if a number to increment it by is written at 0x02025304. Also plays the sound
|
||||
# effect of the ID written at 0x02025306, if one is written there. This will NOT give any items on its own; the item
|
||||
# has to be written by the client into the inventory alongside writing the above-mentioned things.
|
||||
|
||||
# Make sure we didn't hit the lucky one frame before room transitioning wherein Nathan is on top of the room
|
||||
# transition tile.
|
||||
0x0C, 0x88, # ldrh r4, [r1]
|
||||
0x80, 0x20, # movs r0, #0x80
|
||||
0x20, 0x40, # ands r0, r4
|
||||
0x00, 0x28, # cmp r0, #0
|
||||
0x2F, 0xD1, # bne 0x87FFF8A
|
||||
0x11, 0xB4, # push r0, r4
|
||||
# Check the cutscene value to make sure we are not in a cutscene; forcing a textbox while there's already another
|
||||
# textbox on-screen messes things up.
|
||||
0x1E, 0x4A, # ldr r2, =0x2026000
|
||||
0x13, 0x78, # ldrb r3, [r2]
|
||||
0x00, 0x2B, # cmp r0, #0
|
||||
0x29, 0xD1, # bne 0x87FFF88
|
||||
# Check our "delay" timer buffer for a non-zero. If it is, decrement it by one and skip straight to the return part
|
||||
# of this code, as we may have received an item on a frame wherein it's "unsafe" to pop the item textbox.
|
||||
0x16, 0x4A, # ldr r2, =0x2025300
|
||||
0x13, 0x89, # ldrh r3, [r2, #8]
|
||||
0x00, 0x2B, # cmp r0, #0
|
||||
0x02, 0xD0, # beq 0x87FFF42
|
||||
0x01, 0x3B, # subs r3, #1
|
||||
0x13, 0x81, # strh r3, [r2, #8]
|
||||
0x22, 0xE0, # beq 0x87FFF88
|
||||
# Check our first custom "textbox ID" buffers for a non-zero number.
|
||||
0x10, 0x88, # ldrh r0, [r2]
|
||||
0x00, 0x28, # cmp r0, #0
|
||||
0x12, 0xD0, # beq 0x87FFF6E
|
||||
# Increase the "received item index" by the specified number in our "item index amount to increase" buffer.
|
||||
0x93, 0x88, # ldrh r3, [r2, #4]
|
||||
0xD0, 0x32, # adds r2, #0xD0
|
||||
0x11, 0x88, # ldrh r1, [r2]
|
||||
0xC9, 0x18, # adds r1, r1, r3
|
||||
0x11, 0x80, # strh r1, [r2]
|
||||
# Check our second custom "textbox ID" buffers for a non-zero number.
|
||||
0xD0, 0x3A, # subs r2, #0xD0
|
||||
0x51, 0x88, # ldrh r1, [r2, #2]
|
||||
0x00, 0x29, # cmp r1, #0
|
||||
0x01, 0xD0, # beq 0x87FFF5E
|
||||
# If we have a second textbox ID, run the "display two textboxes" function.
|
||||
# Otherwise, run the "display one textbox" function.
|
||||
0x0E, 0x4A, # ldr r2, =0x805F104
|
||||
0x00, 0xE0, # b 0x87FFF60
|
||||
0x0E, 0x4A, # ldr r2, =0x805F0C8
|
||||
0x7B, 0x46, # mov r3, r15
|
||||
0x05, 0x33, # adds r3, #5
|
||||
0x9E, 0x46, # mov r14, r3
|
||||
0x97, 0x46, # mov r15, r2
|
||||
0x09, 0x48, # ldr r0, =0x2025300
|
||||
0x02, 0x21, # movs r1, #2
|
||||
0x01, 0x81, # strh r1, [r0, #8]
|
||||
# Check our "sound effect ID" buffer and run the "play sound" function if it's a non-zero number.
|
||||
0x08, 0x48, # ldr r0, =0x2025300
|
||||
0xC0, 0x88, # ldrh r0, [r0, #6]
|
||||
0x00, 0x28, # cmp r0, #0
|
||||
0x04, 0xD0, # beq 0x87FFF7E
|
||||
0x0B, 0x4A, # ldr r2, =0x8005E80
|
||||
0x7B, 0x46, # mov r3, r15
|
||||
0x05, 0x33, # adds r3, #5
|
||||
0x9E, 0x46, # mov r14, r3
|
||||
0x97, 0x46, # mov r15, r2
|
||||
# Clear all our buffers and return to the "check for Nathan being in a room transition" function we've hooked into.
|
||||
0x03, 0x48, # ldr r0, =0x2025300
|
||||
0x00, 0x21, # movs r1, #0
|
||||
0x01, 0x60, # str r1, [r0]
|
||||
0x41, 0x60, # str r1, [r0, #4]
|
||||
0x11, 0xBC, # pop r0, r4
|
||||
0x04, 0x4A, # ldr r2, =0x8007D68
|
||||
0x00, 0x28, # cmp r0, #0
|
||||
0x97, 0x46, # mov r15, r2
|
||||
# LDR number pool
|
||||
0x00, 0x53, 0x02, 0x02,
|
||||
0x04, 0xF1, 0x05, 0x08,
|
||||
0xC8, 0xF0, 0x05, 0x08,
|
||||
0x68, 0x7D, 0x00, 0x08,
|
||||
0x90, 0x1E, 0x02, 0x02,
|
||||
0x80, 0x5E, 0x00, 0x08,
|
||||
0x00, 0x60, 0x02, 0x02
|
||||
]
|
||||
|
||||
transition_textbox_delayer = [
|
||||
# Sets the remote item textbox delay timer whenever the player screen transitions to ensure the item textbox won't
|
||||
# pop during said transition.
|
||||
0x40, 0x78, # ldrb r0, [r0, #1]
|
||||
0x28, 0x70, # strb r0, [r5]
|
||||
0xF8, 0x6D, # ldr r0, [r7, #0x5C]
|
||||
0x20, 0x18, # adds r0, r4, r0
|
||||
0x02, 0x4A, # ldr r2, =0x2025300
|
||||
0x10, 0x23, # movs r3, #0x10
|
||||
0x13, 0x80, # strh r3, [r2]
|
||||
0x02, 0x4A, # ldr r2, =0x806CE1C
|
||||
0x97, 0x46, # mov r15, r2
|
||||
0x00, 0x00,
|
||||
# LDR number pool
|
||||
0x08, 0x53, 0x02, 0x02,
|
||||
0x1C, 0xCE, 0x06, 0x08,
|
||||
]
|
||||
|
||||
magic_item_sfx_customizer = [
|
||||
# Enables a different sound to be played depending on which Magic Item was picked up. The array starting at 086797C0
|
||||
# contains each 2-byte sound ID for each Magic Item. Putting 0000 for a sound will cause no sound to play; this is
|
||||
# currently used for the dummy AP Items as their sound is played by the "sent" textbox instead.
|
||||
0x70, 0x68, # ldr r0, [r6, #4]
|
||||
0x80, 0x79, # ldrb r0, [r0, #6]
|
||||
0x40, 0x00, # lsl r0, r0, 1
|
||||
0x07, 0x49, # ldr r1, =0x86797C0
|
||||
0x08, 0x5A, # ldrh r0, [r1, r0]
|
||||
0x00, 0x28, # cmp r0, 0
|
||||
0x04, 0xD0, # beq 0x8679818
|
||||
0x03, 0x4A, # ldr r2, =0x8005E80
|
||||
0x7B, 0x46, # mov r3, r15
|
||||
0x05, 0x33, # adds r3, #5
|
||||
0x9E, 0x46, # mov r14, r3
|
||||
0x97, 0x46, # mov r15, r2
|
||||
0x01, 0x48, # ldr r0, =0x8095BEC
|
||||
0x87, 0x46, # mov r15, r0
|
||||
# LDR number pool
|
||||
0x80, 0x5E, 0x00, 0x08,
|
||||
0xEC, 0x5B, 0x09, 0x08,
|
||||
0xC0, 0x97, 0x67, 0x08,
|
||||
]
|
||||
|
||||
start_inventory_giver = [
|
||||
# This replaces AutoDashBoots.ips from standalone CotMR by allowing the player to start with any set of items, not
|
||||
# just the Dash Boots. If playing Magician Mode, they will be given all cards that were not put into the starting
|
||||
# inventory right after this code runs.
|
||||
|
||||
# Magic Items
|
||||
0x13, 0x48, # ldr r0, =0x202572F
|
||||
0x14, 0x49, # ldr r1, =0x8680080
|
||||
0x00, 0x22, # mov r2, #0
|
||||
0x8B, 0x5C, # ldrb r3, [r1, r2]
|
||||
0x83, 0x54, # strb r3, [r0, r2]
|
||||
0x01, 0x32, # adds r2, #1
|
||||
0x08, 0x2A, # cmp r2, #8
|
||||
0xFA, 0xDB, # blt 0x8680006
|
||||
# Max Ups
|
||||
0x11, 0x48, # ldr r0, =0x202572C
|
||||
0x12, 0x49, # ldr r1, =0x8680090
|
||||
0x00, 0x22, # mov r2, #0
|
||||
0x8B, 0x5C, # ldrb r3, [r1, r2]
|
||||
0x83, 0x54, # strb r3, [r0, r2]
|
||||
0x01, 0x32, # adds r2, #1
|
||||
0x03, 0x2A, # cmp r2, #3
|
||||
0xFA, 0xDB, # blt 0x8680016
|
||||
# Cards
|
||||
0x0F, 0x48, # ldr r0, =0x2025674
|
||||
0x10, 0x49, # ldr r1, =0x86800A0
|
||||
0x00, 0x22, # mov r2, #0
|
||||
0x8B, 0x5C, # ldrb r3, [r1, r2]
|
||||
0x83, 0x54, # strb r3, [r0, r2]
|
||||
0x01, 0x32, # adds r2, #1
|
||||
0x14, 0x2A, # cmp r2, #0x14
|
||||
0xFA, 0xDB, # blt 0x8680026
|
||||
# Inventory Items (not currently supported)
|
||||
0x0D, 0x48, # ldr r0, =0x20256ED
|
||||
0x0E, 0x49, # ldr r1, =0x86800C0
|
||||
0x00, 0x22, # mov r2, #0
|
||||
0x8B, 0x5C, # ldrb r3, [r1, r2]
|
||||
0x83, 0x54, # strb r3, [r0, r2]
|
||||
0x01, 0x32, # adds r2, #1
|
||||
0x36, 0x2A, # cmp r2, #36
|
||||
0xFA, 0xDB, # blt 0x8680036
|
||||
# Return to the function that checks for Magician Mode.
|
||||
0xBA, 0x21, # movs r1, #0xBA
|
||||
0x89, 0x00, # lsls r1, r1, #2
|
||||
0x70, 0x18, # adds r0, r6, r1
|
||||
0x04, 0x70, # strb r4, [r0]
|
||||
0x00, 0x4A, # ldr r2, =0x8007F78
|
||||
0x97, 0x46, # mov r15, r2
|
||||
# LDR number pool
|
||||
0x78, 0x7F, 0x00, 0x08,
|
||||
0x2F, 0x57, 0x02, 0x02,
|
||||
0x80, 0x00, 0x68, 0x08,
|
||||
0x2C, 0x57, 0x02, 0x02,
|
||||
0x90, 0x00, 0x68, 0x08,
|
||||
0x74, 0x56, 0x02, 0x02,
|
||||
0xA0, 0x00, 0x68, 0x08,
|
||||
0xED, 0x56, 0x02, 0x02,
|
||||
0xC0, 0x00, 0x68, 0x08,
|
||||
]
|
||||
|
||||
max_max_up_checker = [
|
||||
# Whenever the player picks up a Max Up, this will check to see if they currently have 255 of that particular Max Up
|
||||
# and only increment the number further if they don't. This is necessary for extreme Item Link seeds, as going over
|
||||
# 255 of any Max Up will reset the counter to 0.
|
||||
0x08, 0x78, # ldrb r0, [r1]
|
||||
0xFF, 0x28, # cmp r0, 0xFF
|
||||
0x17, 0xD1, # bne 0x86A0036
|
||||
# If it's an HP Max, refill our HP.
|
||||
0xFF, 0x23, # mov r3, #0xFF
|
||||
0x0B, 0x40, # and r3, r1
|
||||
0x2D, 0x2B, # cmp r3, 0x2D
|
||||
0x03, 0xD1, # bne 0x86A0016
|
||||
0x0D, 0x4A, # ldr r2, =0x202562E
|
||||
0x93, 0x88, # ldrh r3, [r2, #4]
|
||||
0x13, 0x80, # strh r3, [r2]
|
||||
0x11, 0xE0, # b 0x86A003A
|
||||
# If it's an MP Max, refill our MP.
|
||||
0x2E, 0x2B, # cmp r3, 0x2E
|
||||
0x03, 0xD1, # bne 0x86A0022
|
||||
0x0B, 0x4A, # ldr r2, =0x2025636
|
||||
0x93, 0x88, # ldrh r3, [r2, #4]
|
||||
0x13, 0x80, # strh r3, [r2]
|
||||
0x0B, 0xE0, # b 0x86A003A
|
||||
# Else, meaning it's a Hearts Max, add +6 Hearts. If adding +6 Hearts would put us over our current max, set our
|
||||
# current amount to said current max instead.
|
||||
0x0A, 0x4A, # ldr r2, =0x202563C
|
||||
0x13, 0x88, # ldrh r3, [r2]
|
||||
0x06, 0x33, # add r3, #6
|
||||
0x51, 0x88, # ldrh r1, [r2, #2]
|
||||
0x8B, 0x42, # cmp r3, r1
|
||||
0x00, 0xDB, # blt 0x86A0030
|
||||
0x0B, 0x1C, # add r3, r1, #0
|
||||
0x13, 0x80, # strh r3, [r2]
|
||||
0x02, 0xE0, # b 0x86A003A
|
||||
0x00, 0x00,
|
||||
# Increment the Max Up count like normal. Should only get here if the Max Up count was determined to be less than
|
||||
# 255, branching past if not the case.
|
||||
0x01, 0x30, # adds r0, #1
|
||||
0x08, 0x70, # strb r0, [r1]
|
||||
# Return to the function that gives Max Ups normally.
|
||||
0x05, 0x48, # ldr r0, =0x1B3
|
||||
0x00, 0x4A, # ldr r2, =0x805E170
|
||||
0x97, 0x46, # mov r15, r2
|
||||
# LDR number pool
|
||||
0x78, 0xE1, 0x05, 0x08,
|
||||
0x2E, 0x56, 0x02, 0x02,
|
||||
0x36, 0x56, 0x02, 0x02,
|
||||
0x3C, 0x56, 0x02, 0x02,
|
||||
0xB3, 0x01, 0x00, 0x00,
|
||||
]
|
||||
|
||||
maiden_detonator = [
|
||||
# Detonates the iron maidens upon picking up the Maiden Detonator item by setting the "broke iron maidens" flag.
|
||||
0x2A, 0x20, # mov r0, #0x2A
|
||||
0x03, 0x4A, # ldr r2, =0x8007E24
|
||||
0x7B, 0x46, # mov r3, r15
|
||||
0x05, 0x33, # adds r3, #5
|
||||
0x9E, 0x46, # mov r14, r3
|
||||
0x97, 0x46, # mov r15, r2
|
||||
0x01, 0x4A, # ldr r2, =0x8095BE4
|
||||
0x97, 0x46, # mov r15, r2
|
||||
# LDR number pool
|
||||
0x24, 0x7E, 0x00, 0x08,
|
||||
0xE4, 0x5B, 0x09, 0x08,
|
||||
]
|
||||
|
||||
doubleless_roc_midairs_preventer = [
|
||||
# Prevents being able to Roc jump in midair without the Double. Will pass if the jump counter is 0 or if Double is
|
||||
# in the inventory.
|
||||
# Check for Roc Wing in the inventory normally.
|
||||
0x58, 0x18, # add r0, r3, r1
|
||||
0x00, 0x78, # ldrb r0, [r0]
|
||||
0x00, 0x28, # cmp r0, 0
|
||||
0x11, 0xD0, # beq 0x8679A2C
|
||||
# Check the "jumps since last on the ground" counter. Is it 0?
|
||||
# If so, then we are on the ground and can advance to the Kick Boots question. If not, advance to the Double check.
|
||||
0x0B, 0x48, # ldr r0, =0x2000080
|
||||
0x01, 0x78, # ldrb r1, [r0]
|
||||
0x00, 0x29, # cmp r1, 0
|
||||
0x03, 0xD0, # beq 0x8679A18
|
||||
# Check for Double in the inventory. Is it there?
|
||||
# If not, then it's not time to Roc! Otherwise, advance to the next check.
|
||||
0x0A, 0x4A, # ldr r2, =0x202572F
|
||||
0x52, 0x78, # ldrb r2, [r2, 1]
|
||||
0x00, 0x2A, # cmp r2, 0
|
||||
0x09, 0xD0, # beq 0x8679A2C
|
||||
# Check for Kick Boots in the inventory. Are they there?
|
||||
# If they are, then we can definitely Roc! If they aren't, however, then on to the next question...
|
||||
0x08, 0x4A, # ldr r2, =0x202572F
|
||||
0xD2, 0x78, # ldrb r2, [r2, 3]
|
||||
0x00, 0x2A, # cmp r2, 0
|
||||
0x03, 0xD1, # bne 0x8679A28
|
||||
# Is our "jumps since last on the ground" counter 2?
|
||||
# If it is, then we already Double jumped and should not Roc jump as well.
|
||||
# Should always pass if we came here from the "on the ground" 0 check.
|
||||
0x02, 0x29, # cmp r1, 2
|
||||
0x03, 0xD0, # beq 0x8679A2C
|
||||
# If we did not Double jump yet, then set the above-mentioned counter to 2, and now we can finally Roc on!
|
||||
0x02, 0x21, # mov r1, 2
|
||||
0x01, 0x70, # strb r1, [r0]
|
||||
# Go to the "Roc jump" code.
|
||||
0x01, 0x4A, # ldr r2, =0x806B8A8
|
||||
0x97, 0x46, # mov r15, r2
|
||||
# Go to the "don't Roc jump" code.
|
||||
0x01, 0x4A, # ldr r2, =0x806B93C
|
||||
0x97, 0x46, # mov r15, r2
|
||||
# LDR number pool
|
||||
0xA8, 0xB8, 0x06, 0x08,
|
||||
0x3C, 0xB9, 0x06, 0x08,
|
||||
0x80, 0x00, 0x00, 0x02,
|
||||
0x2F, 0x57, 0x02, 0x02,
|
||||
]
|
||||
|
||||
kickless_roc_height_shortener = [
|
||||
# Shortens the amount of time spent rising with Roc Wing if the player doesn't have Kick Boots.
|
||||
0x06, 0x49, # ldr r1, =0x202572F
|
||||
0xC9, 0x78, # ldrb r1, [r1, 3]
|
||||
0x00, 0x29, # cmp r1, 0
|
||||
0x00, 0xD1, # bne 0x8679A6A
|
||||
0x10, 0x20, # mov r0, 0x12
|
||||
0xA8, 0x65, # str r0, [r5, 0x58]
|
||||
# Go back to the Roc jump code.
|
||||
0x00, 0x24, # mov r4, 0
|
||||
0x2C, 0x64, # str r4, [r5, 0x40]
|
||||
0x03, 0x49, # ldr r1, =0x80E03A0
|
||||
0x01, 0x4A, # ldr r2, =0x806B8BC
|
||||
0x97, 0x46, # mov r15, r2
|
||||
0x00, 0x00,
|
||||
# LDR number pool
|
||||
0xBC, 0xB8, 0x06, 0x08,
|
||||
0x2F, 0x57, 0x02, 0x02,
|
||||
0xA0, 0x03, 0x0E, 0x08
|
||||
]
|
||||
|
||||
missing_char_data = {
|
||||
# The pixel data for all ASCII characters missing from the game's dialogue textbox font.
|
||||
|
||||
# Each character consists of 8 bytes, with each byte representing one row of pixels in the character. The bytes are
|
||||
# arranged from top to bottom row going from left to right.
|
||||
|
||||
# Each bit within each byte represents the following pixels within that row:
|
||||
# 8- = -+------
|
||||
# 4- = +-------
|
||||
# 2- = ---+----
|
||||
# 1- = --+-----
|
||||
# -8 = -----+--
|
||||
# -4 = ----+---
|
||||
# -2 = -------+
|
||||
# -1 = ------+-
|
||||
0x396C54: [0x00, 0x9C, 0x9C, 0x18, 0x84, 0x00, 0x00, 0x00], # "
|
||||
0x396C5C: [0x00, 0x18, 0xBD, 0x18, 0x18, 0x18, 0xBD, 0x18], # #
|
||||
0x396C64: [0x00, 0x0C, 0x2D, 0x0C, 0x21, 0x00, 0x00, 0x00], # *
|
||||
0x396C6C: [0x00, 0x20, 0x3C, 0xA0, 0x34, 0x28, 0xB4, 0x20], # $
|
||||
0x396C74: [0x00, 0x34, 0x88, 0x80, 0xB4, 0x88, 0x88, 0x34], # 6
|
||||
0x396C7C: [0x00, 0xBC, 0x88, 0x04, 0x04, 0x20, 0x20, 0x20], # 7
|
||||
0x396CBC: [0x00, 0x34, 0x88, 0x88, 0x3C, 0x08, 0x88, 0x34], # 9
|
||||
0x396CC4: [0x00, 0xC0, 0xC0, 0x00, 0x00, 0x00, 0xC0, 0xC0], # :
|
||||
0x396CCC: [0x00, 0xC0, 0xC0, 0x00, 0xC0, 0xC0, 0x80, 0x40], # ;
|
||||
0x396D0C: [0x00, 0x00, 0x09, 0x24, 0x90, 0x24, 0x09, 0x00], # <
|
||||
0x396D14: [0x00, 0x00, 0xFD, 0x00, 0x00, 0x00, 0xFD, 0x00], # =
|
||||
0x396D1C: [0x00, 0x00, 0xC0, 0x30, 0x0C, 0x30, 0xC0, 0x00], # >
|
||||
0x396D54: [0x00, 0x34, 0x88, 0xAC, 0xA8, 0xAC, 0x80, 0x34], # @
|
||||
0x396D5C: [0x00, 0x34, 0x88, 0x88, 0xA8, 0x8C, 0x88, 0x35], # Q
|
||||
0x396D64: [0x00, 0x40, 0x80, 0x10, 0x20, 0x04, 0x08, 0x01], # \
|
||||
0x396D6C: [0x00, 0x20, 0x14, 0x88, 0x00, 0x00, 0x00, 0x00], # ^
|
||||
0x396D9C: [0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0xFD], # _
|
||||
0x396DA4: [0x00, 0x90, 0x10, 0x00, 0x00, 0x00, 0x00, 0x00], # `
|
||||
0x396DAC: [0x00, 0x08, 0x04, 0x04, 0x20, 0x04, 0x04, 0x08], # {
|
||||
0x396DB4: [0x00, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20, 0x20], # |
|
||||
0x396DBC: [0x00, 0x80, 0x10, 0x10, 0x20, 0x10, 0x10, 0x80], # }
|
||||
0x396DF4: [0x00, 0x00, 0x00, 0x90, 0x61, 0x0C, 0x00, 0x00], # ~
|
||||
}
|
||||
|
||||
extra_item_sprites = [
|
||||
# The VRAM data for all the extra item sprites, including the Archipelago Items.
|
||||
|
||||
# NOTE: The Archipelago logo is © 2022 by Krista Corkos and Christopher Wilson
|
||||
# and licensed under Attribution-NonCommercial 4.0 International.
|
||||
# See LICENSES.txt at the root of this apworld's directory for more licensing information.
|
||||
|
||||
# Maiden Detonator
|
||||
0x00, 0x00, 0x00, 0x11, 0x00, 0x00, 0x10, 0xCC, 0x00, 0x00, 0xC1, 0xBB, 0x00, 0x10, 0x1C, 0xB8,
|
||||
0x00, 0x10, 0x1C, 0xB1, 0x00, 0x10, 0xBC, 0xBB, 0x00, 0x00, 0x11, 0x11, 0x00, 0x10, 0xCC, 0xBB,
|
||||
0x11, 0x00, 0x00, 0x00, 0xCC, 0x01, 0x00, 0x00, 0xBB, 0x1C, 0x00, 0x00, 0x8B, 0xC1, 0x01, 0x00,
|
||||
0x1B, 0xC1, 0x01, 0x00, 0xBB, 0xCB, 0x01, 0x00, 0x11, 0x11, 0x00, 0x00, 0xBB, 0xCC, 0x01, 0x00,
|
||||
0x00, 0x10, 0x11, 0x11, 0x00, 0xC1, 0xBC, 0x1B, 0x00, 0x10, 0x11, 0x11, 0x00, 0xC1, 0xBC, 0x1B,
|
||||
0x00, 0x10, 0x11, 0x11, 0x00, 0xC1, 0xBC, 0x1B, 0x00, 0xC1, 0xBC, 0x1B, 0x00, 0x10, 0x11, 0x01,
|
||||
0x11, 0x11, 0x01, 0x00, 0xB1, 0xCB, 0x1C, 0x00, 0x11, 0x11, 0x01, 0x00, 0xB1, 0xCB, 0x1C, 0x00,
|
||||
0x11, 0x11, 0x01, 0x00, 0xB1, 0xCB, 0x1C, 0x00, 0xB1, 0xCB, 0x1C, 0x00, 0x10, 0x11, 0x01, 0x00,
|
||||
# Archipelago Filler
|
||||
0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x20, 0x88, 0x00, 0x22, 0x82, 0x88, 0x20, 0x66, 0x26, 0x88,
|
||||
0x62, 0x66, 0x62, 0x82, 0x62, 0x66, 0x66, 0x82, 0x62, 0x22, 0x62, 0x22, 0x20, 0xAA, 0x2A, 0x00,
|
||||
0x02, 0x00, 0x00, 0x00, 0x28, 0x00, 0x00, 0x00, 0x82, 0x22, 0x02, 0x00, 0x28, 0xCC, 0x2C, 0x00,
|
||||
0xC2, 0xCC, 0xC2, 0x02, 0xC2, 0xCC, 0xCC, 0x02, 0xC2, 0x22, 0xC2, 0x02, 0x20, 0xFF, 0x2F, 0x00,
|
||||
0xA2, 0xAA, 0xA2, 0x02, 0xA2, 0xAA, 0xAA, 0x22, 0xA2, 0xAA, 0x2A, 0x77, 0x20, 0xAA, 0x72, 0x77,
|
||||
0x00, 0x22, 0x72, 0x77, 0x00, 0x00, 0x72, 0x77, 0x00, 0x00, 0x20, 0x77, 0x00, 0x00, 0x00, 0x22,
|
||||
0xF2, 0xFF, 0xF2, 0x02, 0xF2, 0xFF, 0xFF, 0x02, 0x27, 0xFF, 0xFF, 0x02, 0x72, 0xF2, 0x2F, 0x00,
|
||||
0x77, 0x22, 0x02, 0x00, 0x77, 0x02, 0x00, 0x00, 0x27, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00,
|
||||
# Archipelago Useful
|
||||
0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x20, 0x88, 0x00, 0x22, 0x82, 0x88, 0x20, 0x66, 0x26, 0x88,
|
||||
0x62, 0x66, 0x62, 0x82, 0x62, 0x66, 0x66, 0x82, 0x62, 0x22, 0x62, 0x22, 0x20, 0xAA, 0x2A, 0x00,
|
||||
0x02, 0xAA, 0x0A, 0x00, 0x28, 0x9A, 0x0A, 0x00, 0xAA, 0x9A, 0xAA, 0x0A, 0x9A, 0x99, 0x99, 0x0A,
|
||||
0xAA, 0x9A, 0xAA, 0x0A, 0xC2, 0x9A, 0xCA, 0x02, 0xC2, 0xAA, 0xCA, 0x02, 0x20, 0xFF, 0x2F, 0x00,
|
||||
0xA2, 0xAA, 0xA2, 0x02, 0xA2, 0xAA, 0xAA, 0x22, 0xA2, 0xAA, 0x2A, 0x77, 0x20, 0xAA, 0x72, 0x77,
|
||||
0x00, 0x22, 0x72, 0x77, 0x00, 0x00, 0x72, 0x77, 0x00, 0x00, 0x20, 0x77, 0x00, 0x00, 0x00, 0x22,
|
||||
0xF2, 0xFF, 0xF2, 0x02, 0xF2, 0xFF, 0xFF, 0x02, 0x27, 0xFF, 0xFF, 0x02, 0x72, 0xF2, 0x2F, 0x00,
|
||||
0x77, 0x22, 0x02, 0x00, 0x77, 0x02, 0x00, 0x00, 0x27, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00,
|
||||
# Archipelago Progression
|
||||
0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x20, 0x88, 0x00, 0x22, 0x82, 0x88, 0x20, 0x66, 0x26, 0x88,
|
||||
0x62, 0x66, 0x62, 0x82, 0x62, 0x66, 0x66, 0x82, 0x62, 0x22, 0x62, 0x22, 0x20, 0xAA, 0x2A, 0x00,
|
||||
0x02, 0x10, 0x00, 0x00, 0x28, 0x71, 0x01, 0x00, 0x12, 0x77, 0x17, 0x00, 0x71, 0x77, 0x77, 0x01,
|
||||
0x11, 0x77, 0x17, 0x01, 0x12, 0x77, 0x17, 0x02, 0x12, 0x11, 0x11, 0x02, 0x20, 0xFF, 0x2F, 0x00,
|
||||
0xA2, 0xAA, 0xA2, 0x02, 0xA2, 0xAA, 0xAA, 0x22, 0xA2, 0xAA, 0x2A, 0x77, 0x20, 0xAA, 0x72, 0x77,
|
||||
0x00, 0x22, 0x72, 0x77, 0x00, 0x00, 0x72, 0x77, 0x00, 0x00, 0x20, 0x77, 0x00, 0x00, 0x00, 0x22,
|
||||
0xF2, 0xFF, 0xF2, 0x02, 0xF2, 0xFF, 0xFF, 0x02, 0x27, 0xFF, 0xFF, 0x02, 0x72, 0xF2, 0x2F, 0x00,
|
||||
0x77, 0x22, 0x02, 0x00, 0x77, 0x02, 0x00, 0x00, 0x27, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00,
|
||||
# Archipelago Trap
|
||||
0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x20, 0x88, 0x00, 0x22, 0x82, 0x82, 0x20, 0x66, 0x26, 0x88,
|
||||
0x62, 0x62, 0x66, 0x82, 0x62, 0x66, 0x66, 0x82, 0x62, 0x22, 0x62, 0x22, 0x20, 0xAA, 0x2A, 0x00,
|
||||
0x02, 0x10, 0x00, 0x00, 0x28, 0x71, 0x01, 0x00, 0x18, 0x77, 0x17, 0x00, 0x71, 0x77, 0x77, 0x01,
|
||||
0x11, 0x77, 0x17, 0x01, 0x12, 0x77, 0x17, 0x02, 0x12, 0x11, 0x11, 0x02, 0x20, 0xFF, 0x2F, 0x00,
|
||||
0xA2, 0xA2, 0xAA, 0x02, 0xA2, 0xAA, 0xAA, 0x22, 0xA2, 0xAA, 0x2A, 0x77, 0x20, 0xAA, 0x72, 0x72,
|
||||
0x00, 0x22, 0x72, 0x77, 0x00, 0x00, 0x72, 0x77, 0x00, 0x00, 0x20, 0x77, 0x00, 0x00, 0x00, 0x22,
|
||||
0xF2, 0xF2, 0xFF, 0x02, 0xF2, 0xFF, 0xFF, 0x02, 0x27, 0xFF, 0xFF, 0x02, 0x77, 0xF2, 0x2F, 0x00,
|
||||
0x77, 0x22, 0x02, 0x00, 0x77, 0x02, 0x00, 0x00, 0x27, 0x00, 0x00, 0x00, 0x02, 0x00, 0x00, 0x00,
|
||||
# Archipelago Progression + Useful
|
||||
0x00, 0x00, 0x00, 0x22, 0x00, 0x00, 0x20, 0x88, 0x00, 0x22, 0x82, 0x88, 0x20, 0x66, 0x26, 0x88,
|
||||
0x62, 0x66, 0x62, 0x82, 0x62, 0x66, 0x66, 0x82, 0x62, 0x22, 0x62, 0x22, 0x20, 0xAA, 0x2A, 0x00,
|
||||
0x02, 0x10, 0x00, 0x00, 0x28, 0x71, 0x01, 0x00, 0x12, 0x77, 0x17, 0x00, 0x71, 0x77, 0x77, 0x01,
|
||||
0x11, 0x77, 0x17, 0x01, 0x12, 0x77, 0x17, 0x02, 0x12, 0x11, 0x11, 0x02, 0x20, 0xFF, 0x2F, 0x00,
|
||||
0xA2, 0xAA, 0xA2, 0x02, 0xA2, 0xAA, 0xAA, 0x22, 0xA2, 0xAA, 0x2A, 0x77, 0x20, 0xAA, 0x72, 0x77,
|
||||
0x00, 0x22, 0x72, 0x77, 0x00, 0x00, 0x72, 0x77, 0x00, 0x00, 0x20, 0x77, 0x00, 0x00, 0x00, 0x22,
|
||||
0xF2, 0xFF, 0xF2, 0x02, 0xF2, 0xAA, 0xFA, 0x02, 0x27, 0x9A, 0xFA, 0x02, 0xAA, 0x9A, 0xAA, 0x0A,
|
||||
0x9A, 0x99, 0x99, 0x0A, 0xAA, 0x9A, 0xAA, 0x0A, 0x27, 0x9A, 0x0A, 0x00, 0x02, 0xAA, 0x0A, 0x00,
|
||||
# Hourglass (Specifically used to represent Max Sand from Timespinner)
|
||||
0x00, 0x00, 0xFF, 0xFF, 0x00, 0xF0, 0xEE, 0xCC, 0x00, 0xF0, 0x43, 0x42, 0x00, 0xF0, 0x12, 0x11,
|
||||
0x00, 0x00, 0x1F, 0x11, 0x00, 0x00, 0x2F, 0x88, 0x00, 0x00, 0xF0, 0x82, 0x00, 0x00, 0x00, 0x1F,
|
||||
0xFF, 0xFF, 0x00, 0x00, 0xCC, 0xEE, 0x0F, 0x00, 0x42, 0x34, 0x0F, 0x00, 0x11, 0x21, 0x0F, 0x00,
|
||||
0x11, 0xF1, 0x00, 0x00, 0x98, 0xF2, 0x00, 0x00, 0x29, 0x0F, 0x00, 0x00, 0xF9, 0x00, 0x00, 0x00,
|
||||
0x00, 0x00, 0x00, 0x1F, 0x00, 0x00, 0xF0, 0x81, 0x00, 0x00, 0x2F, 0x81, 0x00, 0x00, 0x1F, 0x88,
|
||||
0x00, 0xF0, 0x12, 0xA9, 0x00, 0xF0, 0x43, 0x24, 0x00, 0xF0, 0xEE, 0xCC, 0x00, 0x00, 0xFF, 0xFF,
|
||||
0xF9, 0x00, 0x00, 0x00, 0x19, 0x0F, 0x00, 0x00, 0x99, 0xF2, 0x00, 0x00, 0xA9, 0xF1, 0x00, 0x00,
|
||||
0xAA, 0x21, 0x0F, 0x00, 0x42, 0x34, 0x0F, 0x00, 0xCC, 0xEE, 0x0F, 0x00, 0xFF, 0xFF, 0x00, 0x00,
|
||||
]
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user