mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-10 09:33:46 -07:00
Compare commits
45 Commits
NewSoupVi-
...
core_dyn_l
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
dc8de5696b | ||
|
|
29591f614d | ||
|
|
b0a61be9df | ||
|
|
7c00c9a49d | ||
|
|
1365bd7a0a | ||
|
|
6e5adc7abd | ||
|
|
c97e4866dd | ||
|
|
8444ffa0c7 | ||
|
|
2fb59d39c9 | ||
|
|
b5343a36ff | ||
|
|
d7a0f4cb4c | ||
|
|
77d35b95e2 | ||
|
|
b605fb1032 | ||
|
|
a5231a27cc | ||
|
|
1454bacfdd | ||
|
|
ed4e44b994 | ||
|
|
d36c983461 | ||
|
|
05aa96a335 | ||
|
|
6f2464d4ad | ||
|
|
91185f4f7c | ||
|
|
1371c63a8d | ||
|
|
30b414429f | ||
|
|
ce210cd4ee | ||
|
|
8923b06a49 | ||
|
|
b783eab1e8 | ||
|
|
b972e8c071 | ||
|
|
faeb54224e | ||
|
|
1ba7700283 | ||
|
|
710cf4ebba | ||
|
|
82260d728f | ||
|
|
62e4285924 | ||
|
|
ce78c75999 | ||
|
|
c022c742b5 | ||
|
|
3cb5219e09 | ||
|
|
5d30d16e09 | ||
|
|
4780fd9974 | ||
|
|
3ba0576cf6 | ||
|
|
283d1ab7e8 | ||
|
|
78bc7b8156 | ||
|
|
a07ddb4371 | ||
|
|
4395c608e8 | ||
|
|
f4322242a1 | ||
|
|
a3711eb463 | ||
|
|
6656528d78 | ||
|
|
e1f16c6721 |
1
.gitattributes
vendored
1
.gitattributes
vendored
@@ -1 +1,2 @@
|
||||
worlds/blasphemous/region_data.py linguist-generated=true
|
||||
worlds/yachtdice/YachtWeights.py linguist-generated=true
|
||||
|
||||
8
.github/workflows/build.yml
vendored
8
.github/workflows/build.yml
vendored
@@ -24,14 +24,14 @@ env:
|
||||
jobs:
|
||||
# build-release-macos: # LF volunteer
|
||||
|
||||
build-win-py310: # RCs will still be built and signed by hand
|
||||
build-win: # RCs will still be built and signed by hand
|
||||
runs-on: windows-latest
|
||||
steps:
|
||||
- uses: actions/checkout@v4
|
||||
- name: Install python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.10'
|
||||
python-version: '3.12'
|
||||
- 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,10 +111,10 @@ jobs:
|
||||
- name: Get a recent python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
python-version: '3.12'
|
||||
- name: Install build-time dependencies
|
||||
run: |
|
||||
echo "PYTHON=python3.11" >> $GITHUB_ENV
|
||||
echo "PYTHON=python3.12" >> $GITHUB_ENV
|
||||
wget -nv https://github.com/AppImage/AppImageKit/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage
|
||||
chmod a+rx appimagetool-x86_64.AppImage
|
||||
./appimagetool-x86_64.AppImage --appimage-extract
|
||||
|
||||
4
.github/workflows/release.yml
vendored
4
.github/workflows/release.yml
vendored
@@ -44,10 +44,10 @@ jobs:
|
||||
- name: Get a recent python
|
||||
uses: actions/setup-python@v5
|
||||
with:
|
||||
python-version: '3.11'
|
||||
python-version: '3.12'
|
||||
- name: Install build-time dependencies
|
||||
run: |
|
||||
echo "PYTHON=python3.11" >> $GITHUB_ENV
|
||||
echo "PYTHON=python3.12" >> $GITHUB_ENV
|
||||
wget -nv https://github.com/AppImage/AppImageKit/releases/download/$APPIMAGETOOL_VERSION/appimagetool-x86_64.AppImage
|
||||
chmod a+rx appimagetool-x86_64.AppImage
|
||||
./appimagetool-x86_64.AppImage --appimage-extract
|
||||
|
||||
@@ -1110,7 +1110,7 @@ class Region:
|
||||
return exit_
|
||||
|
||||
def add_exits(self, exits: Union[Iterable[str], Dict[str, Optional[str]]],
|
||||
rules: Dict[str, Callable[[CollectionState], bool]] = None) -> None:
|
||||
rules: Dict[str, Callable[[CollectionState], bool]] = None) -> List[Entrance]:
|
||||
"""
|
||||
Connects current region to regions in exit dictionary. Passed region names must exist first.
|
||||
|
||||
@@ -1120,10 +1120,14 @@ class Region:
|
||||
"""
|
||||
if not isinstance(exits, Dict):
|
||||
exits = dict.fromkeys(exits)
|
||||
for connecting_region, name in exits.items():
|
||||
self.connect(self.multiworld.get_region(connecting_region, self.player),
|
||||
name,
|
||||
rules[connecting_region] if rules and connecting_region in rules else None)
|
||||
return [
|
||||
self.connect(
|
||||
self.multiworld.get_region(connecting_region, self.player),
|
||||
name,
|
||||
rules[connecting_region] if rules and connecting_region in rules else None,
|
||||
)
|
||||
for connecting_region, name in exits.items()
|
||||
]
|
||||
|
||||
def __repr__(self):
|
||||
return self.multiworld.get_name_string_for_object(self) if self.multiworld else f'{self.name} (Player {self.player})'
|
||||
@@ -1262,6 +1266,10 @@ class Item:
|
||||
def trap(self) -> bool:
|
||||
return ItemClassification.trap in self.classification
|
||||
|
||||
@property
|
||||
def filler(self) -> bool:
|
||||
return not (self.advancement or self.useful or self.trap)
|
||||
|
||||
@property
|
||||
def excludable(self) -> bool:
|
||||
return not (self.advancement or self.useful)
|
||||
@@ -1384,14 +1392,21 @@ class Spoiler:
|
||||
|
||||
# second phase, sphere 0
|
||||
removed_precollected: List[Item] = []
|
||||
for item in (i for i in chain.from_iterable(multiworld.precollected_items.values()) if i.advancement):
|
||||
logging.debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player)
|
||||
multiworld.precollected_items[item.player].remove(item)
|
||||
multiworld.state.remove(item)
|
||||
if not multiworld.can_beat_game():
|
||||
multiworld.push_precollected(item)
|
||||
else:
|
||||
removed_precollected.append(item)
|
||||
|
||||
for precollected_items in multiworld.precollected_items.values():
|
||||
# The list of items is mutated by removing one item at a time to determine if each item is required to beat
|
||||
# the game, and re-adding that item if it was required, so a copy needs to be made before iterating.
|
||||
for item in precollected_items.copy():
|
||||
if not item.advancement:
|
||||
continue
|
||||
logging.debug('Checking if %s (Player %d) is required to beat the game.', item.name, item.player)
|
||||
precollected_items.remove(item)
|
||||
multiworld.state.remove(item)
|
||||
if not multiworld.can_beat_game():
|
||||
# Add the item back into `precollected_items` and collect it into `multiworld.state`.
|
||||
multiworld.push_precollected(item)
|
||||
else:
|
||||
removed_precollected.append(item)
|
||||
|
||||
# we are now down to just the required progress items in collection_spheres. Unfortunately
|
||||
# the previous pruning stage could potentially have made certain items dependant on others
|
||||
@@ -1530,7 +1545,7 @@ class Spoiler:
|
||||
[f" {location}: {item}" for (location, item) in sphere.items()] if isinstance(sphere, dict) else
|
||||
[f" {item}" for item in sphere])) for (sphere_nr, sphere) in self.playthrough.items()]))
|
||||
if self.unreachables:
|
||||
outfile.write('\n\nUnreachable Items:\n\n')
|
||||
outfile.write('\n\nUnreachable Progression Items:\n\n')
|
||||
outfile.write(
|
||||
'\n'.join(['%s: %s' % (unreachable.item, unreachable) for unreachable in self.unreachables]))
|
||||
|
||||
|
||||
@@ -23,7 +23,7 @@ if __name__ == "__main__":
|
||||
|
||||
from MultiServer import CommandProcessor
|
||||
from NetUtils import (Endpoint, decode, NetworkItem, encode, JSONtoTextParser, ClientStatus, Permission, NetworkSlot,
|
||||
RawJSONtoTextParser, add_json_text, add_json_location, add_json_item, JSONTypes, SlotType)
|
||||
RawJSONtoTextParser, add_json_text, add_json_location, add_json_item, JSONTypes, HintStatus, SlotType)
|
||||
from Utils import Version, stream_input, async_start
|
||||
from worlds import network_data_package, AutoWorldRegister
|
||||
import os
|
||||
@@ -412,6 +412,7 @@ class CommonContext:
|
||||
await self.server.socket.close()
|
||||
if self.server_task is not None:
|
||||
await self.server_task
|
||||
self.ui.update_hints()
|
||||
|
||||
async def send_msgs(self, msgs: typing.List[typing.Any]) -> None:
|
||||
""" `msgs` JSON serializable """
|
||||
@@ -551,7 +552,14 @@ class CommonContext:
|
||||
await self.ui_task
|
||||
if self.input_task:
|
||||
self.input_task.cancel()
|
||||
|
||||
|
||||
# Hints
|
||||
def update_hint(self, location: int, finding_player: int, status: typing.Optional[HintStatus]) -> None:
|
||||
msg = {"cmd": "UpdateHint", "location": location, "player": finding_player}
|
||||
if status is not None:
|
||||
msg["status"] = status
|
||||
async_start(self.send_msgs([msg]), name="update_hint")
|
||||
|
||||
# DataPackage
|
||||
async def prepare_data_package(self, relevant_games: typing.Set[str],
|
||||
remote_date_package_versions: typing.Dict[str, int],
|
||||
|
||||
39
Fill.py
39
Fill.py
@@ -978,15 +978,32 @@ def distribute_planned(multiworld: MultiWorld) -> None:
|
||||
multiworld.random.shuffle(items)
|
||||
count = 0
|
||||
err: typing.List[str] = []
|
||||
successful_pairs: typing.List[typing.Tuple[Item, Location]] = []
|
||||
successful_pairs: typing.List[typing.Tuple[int, Item, Location]] = []
|
||||
claimed_indices: typing.Set[typing.Optional[int]] = set()
|
||||
for item_name in items:
|
||||
item = multiworld.worlds[player].create_item(item_name)
|
||||
index_to_delete: typing.Optional[int] = None
|
||||
if from_pool:
|
||||
try:
|
||||
# If from_pool, try to find an existing item with this name & player in the itempool and use it
|
||||
index_to_delete, item = next(
|
||||
(i, item) for i, item in enumerate(multiworld.itempool)
|
||||
if item.player == player and item.name == item_name and i not in claimed_indices
|
||||
)
|
||||
except StopIteration:
|
||||
warn(
|
||||
f"Could not remove {item_name} from pool for {multiworld.player_name[player]} as it's already missing from it.",
|
||||
placement['force'])
|
||||
item = multiworld.worlds[player].create_item(item_name)
|
||||
else:
|
||||
item = multiworld.worlds[player].create_item(item_name)
|
||||
|
||||
for location in reversed(candidates):
|
||||
if (location.address is None) == (item.code is None): # either both None or both not None
|
||||
if not location.item:
|
||||
if location.item_rule(item):
|
||||
if location.can_fill(multiworld.state, item, False):
|
||||
successful_pairs.append((item, location))
|
||||
successful_pairs.append((index_to_delete, item, location))
|
||||
claimed_indices.add(index_to_delete)
|
||||
candidates.remove(location)
|
||||
count = count + 1
|
||||
break
|
||||
@@ -998,6 +1015,7 @@ def distribute_planned(multiworld: MultiWorld) -> None:
|
||||
err.append(f"Cannot place {item_name} into already filled location {location}.")
|
||||
else:
|
||||
err.append(f"Mismatch between {item_name} and {location}, only one is an event.")
|
||||
|
||||
if count == maxcount:
|
||||
break
|
||||
if count < placement['count']['min']:
|
||||
@@ -1005,17 +1023,16 @@ def distribute_planned(multiworld: MultiWorld) -> None:
|
||||
failed(
|
||||
f"Plando block failed to place {m - count} of {m} item(s) for {multiworld.player_name[player]}, error(s): {' '.join(err)}",
|
||||
placement['force'])
|
||||
for (item, location) in successful_pairs:
|
||||
|
||||
# Sort indices in reverse so we can remove them one by one
|
||||
successful_pairs = sorted(successful_pairs, key=lambda successful_pair: successful_pair[0] or 0, reverse=True)
|
||||
|
||||
for (index, item, location) in successful_pairs:
|
||||
multiworld.push_item(location, item, collect=False)
|
||||
location.locked = True
|
||||
logging.debug(f"Plando placed {item} at {location}")
|
||||
if from_pool:
|
||||
try:
|
||||
multiworld.itempool.remove(item)
|
||||
except ValueError:
|
||||
warn(
|
||||
f"Could not remove {item} from pool for {multiworld.player_name[player]} as it's already missing from it.",
|
||||
placement['force'])
|
||||
if index is not None: # If this item is from_pool and was found in the pool, remove it.
|
||||
multiworld.itempool.pop(index)
|
||||
|
||||
except Exception as e:
|
||||
raise Exception(
|
||||
|
||||
71
Main.py
71
Main.py
@@ -153,45 +153,38 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
|
||||
# remove starting inventory from pool items.
|
||||
# Because some worlds don't actually create items during create_items this has to be as late as possible.
|
||||
if any(getattr(multiworld.worlds[player].options, "start_inventory_from_pool", None) for player in multiworld.player_ids):
|
||||
new_items: List[Item] = []
|
||||
old_items: List[Item] = []
|
||||
depletion_pool: Dict[int, Dict[str, int]] = {
|
||||
player: getattr(multiworld.worlds[player].options,
|
||||
"start_inventory_from_pool",
|
||||
StartInventoryPool({})).value.copy()
|
||||
for player in multiworld.player_ids
|
||||
}
|
||||
for player, items in depletion_pool.items():
|
||||
player_world: AutoWorld.World = multiworld.worlds[player]
|
||||
for count in items.values():
|
||||
for _ in range(count):
|
||||
new_items.append(player_world.create_filler())
|
||||
target: int = sum(sum(items.values()) for items in depletion_pool.values())
|
||||
for i, item in enumerate(multiworld.itempool):
|
||||
if depletion_pool[item.player].get(item.name, 0):
|
||||
target -= 1
|
||||
depletion_pool[item.player][item.name] -= 1
|
||||
# quick abort if we have found all items
|
||||
if not target:
|
||||
old_items.extend(multiworld.itempool[i+1:])
|
||||
break
|
||||
else:
|
||||
old_items.append(item)
|
||||
fallback_inventory = StartInventoryPool({})
|
||||
depletion_pool: Dict[int, Dict[str, int]] = {
|
||||
player: getattr(multiworld.worlds[player].options, "start_inventory_from_pool", fallback_inventory).value.copy()
|
||||
for player in multiworld.player_ids
|
||||
}
|
||||
target_per_player = {
|
||||
player: sum(target_items.values()) for player, target_items in depletion_pool.items() if target_items
|
||||
}
|
||||
|
||||
# leftovers?
|
||||
if target:
|
||||
for player, remaining_items in depletion_pool.items():
|
||||
remaining_items = {name: count for name, count in remaining_items.items() if count}
|
||||
if remaining_items:
|
||||
logger.warning(f"{multiworld.get_player_name(player)}"
|
||||
f" is trying to remove items from their pool that don't exist: {remaining_items}")
|
||||
# find all filler we generated for the current player and remove until it matches
|
||||
removables = [item for item in new_items if item.player == player]
|
||||
for _ in range(sum(remaining_items.values())):
|
||||
new_items.remove(removables.pop())
|
||||
assert len(multiworld.itempool) == len(new_items + old_items), "Item Pool amounts should not change."
|
||||
multiworld.itempool[:] = new_items + old_items
|
||||
if target_per_player:
|
||||
new_itempool: List[Item] = []
|
||||
|
||||
# Make new itempool with start_inventory_from_pool items removed
|
||||
for item in multiworld.itempool:
|
||||
if depletion_pool[item.player].get(item.name, 0):
|
||||
depletion_pool[item.player][item.name] -= 1
|
||||
else:
|
||||
new_itempool.append(item)
|
||||
|
||||
# Create filler in place of the removed items, warn if any items couldn't be found in the multiworld itempool
|
||||
for player, target in target_per_player.items():
|
||||
unfound_items = {item: count for item, count in depletion_pool[player].items() if count}
|
||||
|
||||
if unfound_items:
|
||||
player_name = multiworld.get_player_name(player)
|
||||
logger.warning(f"{player_name} tried to remove items from their pool that don't exist: {unfound_items}")
|
||||
|
||||
needed_items = target_per_player[player] - sum(unfound_items.values())
|
||||
new_itempool += [multiworld.worlds[player].create_filler() for _ in range(needed_items)]
|
||||
|
||||
assert len(multiworld.itempool) == len(new_itempool), "Item Pool amounts should not change."
|
||||
multiworld.itempool[:] = new_itempool
|
||||
|
||||
multiworld.link_items()
|
||||
|
||||
@@ -276,7 +269,7 @@ def main(args, seed=None, baked_server_options: Optional[Dict[str, object]] = No
|
||||
def precollect_hint(location):
|
||||
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)
|
||||
location.item.code, False, entrance, location.item.flags, False)
|
||||
precollected_hints[location.player].add(hint)
|
||||
if location.item.player not in multiworld.groups:
|
||||
precollected_hints[location.item.player].add(hint)
|
||||
|
||||
174
MultiServer.py
174
MultiServer.py
@@ -41,7 +41,8 @@ import NetUtils
|
||||
import Utils
|
||||
from Utils import version_tuple, restricted_loads, Version, async_start, get_intended_text
|
||||
from NetUtils import Endpoint, ClientStatus, NetworkItem, decode, encode, NetworkPlayer, Permission, NetworkSlot, \
|
||||
SlotType, LocationStore
|
||||
SlotType, LocationStore, Hint, HintStatus
|
||||
from BaseClasses import ItemClassification
|
||||
|
||||
min_client_version = Version(0, 1, 6)
|
||||
colorama.init()
|
||||
@@ -228,7 +229,7 @@ class Context:
|
||||
self.hint_cost = hint_cost
|
||||
self.location_check_points = location_check_points
|
||||
self.hints_used = collections.defaultdict(int)
|
||||
self.hints: typing.Dict[team_slot, typing.Set[NetUtils.Hint]] = collections.defaultdict(set)
|
||||
self.hints: typing.Dict[team_slot, typing.Set[Hint]] = collections.defaultdict(set)
|
||||
self.release_mode: str = release_mode
|
||||
self.remaining_mode: str = remaining_mode
|
||||
self.collect_mode: str = collect_mode
|
||||
@@ -656,13 +657,29 @@ class Context:
|
||||
return max(1, int(self.hint_cost * 0.01 * len(self.locations[slot])))
|
||||
return 0
|
||||
|
||||
def recheck_hints(self, team: typing.Optional[int] = None, slot: typing.Optional[int] = None):
|
||||
def recheck_hints(self, team: typing.Optional[int] = None, slot: typing.Optional[int] = None,
|
||||
changed: typing.Optional[typing.Set[team_slot]] = None) -> None:
|
||||
"""Refreshes the hints for the specified team/slot. Providing 'None' for either team or slot
|
||||
will refresh all teams or all slots respectively. If a set is passed for 'changed', each (team,slot)
|
||||
pair that has at least one hint modified will be added to the set.
|
||||
"""
|
||||
for hint_team, hint_slot in self.hints:
|
||||
if (team is None or team == hint_team) and (slot is None or slot == hint_slot):
|
||||
self.hints[hint_team, hint_slot] = {
|
||||
hint.re_check(self, hint_team) for hint in
|
||||
self.hints[hint_team, hint_slot]
|
||||
}
|
||||
if team != hint_team and team is not None:
|
||||
continue # Check specified team only, all if team is None
|
||||
if slot != hint_slot and slot is not None:
|
||||
continue # Check specified slot only, all if slot is None
|
||||
new_hints: typing.Set[Hint] = set()
|
||||
for hint in self.hints[hint_team, hint_slot]:
|
||||
new_hint = hint.re_check(self, hint_team)
|
||||
new_hints.add(new_hint)
|
||||
if hint == new_hint:
|
||||
continue
|
||||
for player in self.slot_set(hint.receiving_player) | {hint.finding_player}:
|
||||
if changed is not None:
|
||||
changed.add((hint_team,player))
|
||||
if slot is not None and slot != player:
|
||||
self.replace_hint(hint_team, player, hint, new_hint)
|
||||
self.hints[hint_team, hint_slot] = new_hints
|
||||
|
||||
def get_rechecked_hints(self, team: int, slot: int):
|
||||
self.recheck_hints(team, slot)
|
||||
@@ -711,7 +728,7 @@ class Context:
|
||||
else:
|
||||
return self.player_names[team, slot]
|
||||
|
||||
def notify_hints(self, team: int, hints: typing.List[NetUtils.Hint], only_new: bool = False,
|
||||
def notify_hints(self, team: int, hints: typing.List[Hint], only_new: bool = False,
|
||||
recipients: typing.Sequence[int] = None):
|
||||
"""Send and remember hints."""
|
||||
if only_new:
|
||||
@@ -749,6 +766,17 @@ class Context:
|
||||
for client in clients:
|
||||
async_start(self.send_msgs(client, client_hints))
|
||||
|
||||
def get_hint(self, team: int, finding_player: int, seeked_location: int) -> typing.Optional[Hint]:
|
||||
for hint in self.hints[team, finding_player]:
|
||||
if hint.location == seeked_location:
|
||||
return hint
|
||||
return None
|
||||
|
||||
def replace_hint(self, team: int, slot: int, old_hint: Hint, new_hint: Hint) -> None:
|
||||
if old_hint in self.hints[team, slot]:
|
||||
self.hints[team, slot].remove(old_hint)
|
||||
self.hints[team, slot].add(new_hint)
|
||||
|
||||
# "events"
|
||||
|
||||
def on_goal_achieved(self, client: Client):
|
||||
@@ -1050,14 +1078,15 @@ def register_location_checks(ctx: Context, team: int, slot: int, locations: typi
|
||||
"hint_points": get_slot_points(ctx, team, slot),
|
||||
"checked_locations": new_locations, # send back new checks only
|
||||
}])
|
||||
old_hints = ctx.hints[team, slot].copy()
|
||||
ctx.recheck_hints(team, slot)
|
||||
if old_hints != ctx.hints[team, slot]:
|
||||
ctx.on_changed_hints(team, slot)
|
||||
updated_slots: typing.Set[tuple[int, int]] = set()
|
||||
ctx.recheck_hints(team, slot, updated_slots)
|
||||
for hint_team, hint_slot in updated_slots:
|
||||
ctx.on_changed_hints(hint_team, hint_slot)
|
||||
ctx.save()
|
||||
|
||||
|
||||
def collect_hints(ctx: Context, team: int, slot: int, item: typing.Union[int, str]) -> typing.List[NetUtils.Hint]:
|
||||
def collect_hints(ctx: Context, team: int, slot: int, item: typing.Union[int, str], auto_status: HintStatus) \
|
||||
-> typing.List[Hint]:
|
||||
hints = []
|
||||
slots: typing.Set[int] = {slot}
|
||||
for group_id, group in ctx.groups.items():
|
||||
@@ -1067,31 +1096,58 @@ def collect_hints(ctx: Context, team: int, slot: int, item: typing.Union[int, st
|
||||
seeked_item_id = item if isinstance(item, int) else ctx.item_names_for_game(ctx.games[slot])[item]
|
||||
for finding_player, location_id, item_id, receiving_player, item_flags \
|
||||
in ctx.locations.find_item(slots, seeked_item_id):
|
||||
found = location_id in ctx.location_checks[team, finding_player]
|
||||
entrance = ctx.er_hint_data.get(finding_player, {}).get(location_id, "")
|
||||
hints.append(NetUtils.Hint(receiving_player, finding_player, location_id, item_id, found, entrance,
|
||||
item_flags))
|
||||
prev_hint = ctx.get_hint(team, slot, location_id)
|
||||
if prev_hint:
|
||||
hints.append(prev_hint)
|
||||
else:
|
||||
found = location_id in ctx.location_checks[team, finding_player]
|
||||
entrance = ctx.er_hint_data.get(finding_player, {}).get(location_id, "")
|
||||
new_status = auto_status
|
||||
if found:
|
||||
new_status = HintStatus.HINT_FOUND
|
||||
elif item_flags & ItemClassification.trap:
|
||||
new_status = HintStatus.HINT_AVOID
|
||||
hints.append(Hint(receiving_player, finding_player, location_id, item_id, found, entrance,
|
||||
item_flags, new_status))
|
||||
|
||||
return hints
|
||||
|
||||
|
||||
def collect_hint_location_name(ctx: Context, team: int, slot: int, location: str) -> typing.List[NetUtils.Hint]:
|
||||
def collect_hint_location_name(ctx: Context, team: int, slot: int, location: str, auto_status: HintStatus) \
|
||||
-> typing.List[Hint]:
|
||||
seeked_location: int = ctx.location_names_for_game(ctx.games[slot])[location]
|
||||
return collect_hint_location_id(ctx, team, slot, seeked_location)
|
||||
return collect_hint_location_id(ctx, team, slot, seeked_location, auto_status)
|
||||
|
||||
|
||||
def collect_hint_location_id(ctx: Context, team: int, slot: int, seeked_location: int) -> typing.List[NetUtils.Hint]:
|
||||
def collect_hint_location_id(ctx: Context, team: int, slot: int, seeked_location: int, auto_status: HintStatus) \
|
||||
-> typing.List[Hint]:
|
||||
prev_hint = ctx.get_hint(team, slot, seeked_location)
|
||||
if prev_hint:
|
||||
return [prev_hint]
|
||||
result = ctx.locations[slot].get(seeked_location, (None, None, None))
|
||||
if any(result):
|
||||
item_id, receiving_player, item_flags = result
|
||||
|
||||
found = seeked_location in ctx.location_checks[team, slot]
|
||||
entrance = ctx.er_hint_data.get(slot, {}).get(seeked_location, "")
|
||||
return [NetUtils.Hint(receiving_player, slot, seeked_location, item_id, found, entrance, item_flags)]
|
||||
new_status = auto_status
|
||||
if found:
|
||||
new_status = HintStatus.HINT_FOUND
|
||||
elif item_flags & ItemClassification.trap:
|
||||
new_status = HintStatus.HINT_AVOID
|
||||
return [Hint(receiving_player, slot, seeked_location, item_id, found, entrance, item_flags,
|
||||
new_status)]
|
||||
return []
|
||||
|
||||
|
||||
def format_hint(ctx: Context, team: int, hint: NetUtils.Hint) -> str:
|
||||
status_names: typing.Dict[HintStatus, str] = {
|
||||
HintStatus.HINT_FOUND: "(found)",
|
||||
HintStatus.HINT_UNSPECIFIED: "(unspecified)",
|
||||
HintStatus.HINT_NO_PRIORITY: "(no priority)",
|
||||
HintStatus.HINT_AVOID: "(avoid)",
|
||||
HintStatus.HINT_PRIORITY: "(priority)",
|
||||
}
|
||||
def format_hint(ctx: Context, team: int, hint: Hint) -> str:
|
||||
text = f"[Hint]: {ctx.player_names[team, hint.receiving_player]}'s " \
|
||||
f"{ctx.item_names[ctx.slot_info[hint.receiving_player].game][hint.item]} is " \
|
||||
f"at {ctx.location_names[ctx.slot_info[hint.finding_player].game][hint.location]} " \
|
||||
@@ -1099,7 +1155,8 @@ def format_hint(ctx: Context, team: int, hint: NetUtils.Hint) -> str:
|
||||
|
||||
if hint.entrance:
|
||||
text += f" at {hint.entrance}"
|
||||
return text + (". (found)" if hint.found else ".")
|
||||
|
||||
return text + ". " + status_names.get(hint.status, "(unknown)")
|
||||
|
||||
|
||||
def json_format_send_event(net_item: NetworkItem, receiving_player: int):
|
||||
@@ -1503,7 +1560,7 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
def get_hints(self, input_text: str, for_location: bool = False) -> bool:
|
||||
points_available = get_client_points(self.ctx, self.client)
|
||||
cost = self.ctx.get_hint_cost(self.client.slot)
|
||||
|
||||
auto_status = HintStatus.HINT_UNSPECIFIED if for_location else HintStatus.HINT_PRIORITY
|
||||
if not input_text:
|
||||
hints = {hint.re_check(self.ctx, self.client.team) for hint in
|
||||
self.ctx.hints[self.client.team, self.client.slot]}
|
||||
@@ -1529,9 +1586,9 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
self.output(f"Sorry, \"{hint_name}\" is marked as non-hintable.")
|
||||
hints = []
|
||||
elif not for_location:
|
||||
hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_id)
|
||||
hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_id, auto_status)
|
||||
else:
|
||||
hints = collect_hint_location_id(self.ctx, self.client.team, self.client.slot, hint_id)
|
||||
hints = collect_hint_location_id(self.ctx, self.client.team, self.client.slot, hint_id, auto_status)
|
||||
|
||||
else:
|
||||
game = self.ctx.games[self.client.slot]
|
||||
@@ -1551,16 +1608,16 @@ class ClientMessageProcessor(CommonCommandProcessor):
|
||||
hints = []
|
||||
for item_name in self.ctx.item_name_groups[game][hint_name]:
|
||||
if item_name in self.ctx.item_names_for_game(game): # ensure item has an ID
|
||||
hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item_name))
|
||||
hints.extend(collect_hints(self.ctx, self.client.team, self.client.slot, item_name, auto_status))
|
||||
elif not for_location and hint_name in self.ctx.item_names_for_game(game): # item name
|
||||
hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_name)
|
||||
hints = collect_hints(self.ctx, self.client.team, self.client.slot, hint_name, auto_status)
|
||||
elif hint_name in self.ctx.location_name_groups[game]: # location group name
|
||||
hints = []
|
||||
for loc_name in self.ctx.location_name_groups[game][hint_name]:
|
||||
if loc_name in self.ctx.location_names_for_game(game):
|
||||
hints.extend(collect_hint_location_name(self.ctx, self.client.team, self.client.slot, loc_name))
|
||||
hints.extend(collect_hint_location_name(self.ctx, self.client.team, self.client.slot, loc_name, auto_status))
|
||||
else: # location name
|
||||
hints = collect_hint_location_name(self.ctx, self.client.team, self.client.slot, hint_name)
|
||||
hints = collect_hint_location_name(self.ctx, self.client.team, self.client.slot, hint_name, auto_status)
|
||||
|
||||
else:
|
||||
self.output(response)
|
||||
@@ -1832,13 +1889,51 @@ async def process_client_cmd(ctx: Context, client: Client, args: dict):
|
||||
|
||||
target_item, target_player, flags = ctx.locations[client.slot][location]
|
||||
if create_as_hint:
|
||||
hints.extend(collect_hint_location_id(ctx, client.team, client.slot, location))
|
||||
hints.extend(collect_hint_location_id(ctx, client.team, client.slot, location,
|
||||
HintStatus.HINT_UNSPECIFIED))
|
||||
locs.append(NetworkItem(target_item, location, target_player, flags))
|
||||
ctx.notify_hints(client.team, hints, only_new=create_as_hint == 2)
|
||||
if locs and create_as_hint:
|
||||
ctx.save()
|
||||
await ctx.send_msgs(client, [{'cmd': 'LocationInfo', 'locations': locs}])
|
||||
|
||||
|
||||
elif cmd == 'UpdateHint':
|
||||
location = args["location"]
|
||||
player = args["player"]
|
||||
status = args["status"]
|
||||
if not isinstance(player, int) or not isinstance(location, int) \
|
||||
or (status is not None and not isinstance(status, int)):
|
||||
await ctx.send_msgs(client,
|
||||
[{'cmd': 'InvalidPacket', "type": "arguments", "text": 'UpdateHint',
|
||||
"original_cmd": cmd}])
|
||||
return
|
||||
hint = ctx.get_hint(client.team, player, location)
|
||||
if not hint:
|
||||
return # Ignored safely
|
||||
if hint.receiving_player != client.slot:
|
||||
await ctx.send_msgs(client,
|
||||
[{'cmd': 'InvalidPacket', "type": "arguments", "text": 'UpdateHint: No Permission',
|
||||
"original_cmd": cmd}])
|
||||
return
|
||||
new_hint = hint
|
||||
if status is None:
|
||||
return
|
||||
try:
|
||||
status = HintStatus(status)
|
||||
except ValueError:
|
||||
await ctx.send_msgs(client,
|
||||
[{'cmd': 'InvalidPacket', "type": "arguments",
|
||||
"text": 'UpdateHint: Invalid Status', "original_cmd": cmd}])
|
||||
return
|
||||
new_hint = new_hint.re_prioritize(ctx, status)
|
||||
if hint == new_hint:
|
||||
return
|
||||
ctx.replace_hint(client.team, hint.finding_player, hint, new_hint)
|
||||
ctx.replace_hint(client.team, hint.receiving_player, hint, new_hint)
|
||||
ctx.save()
|
||||
ctx.on_changed_hints(client.team, hint.finding_player)
|
||||
ctx.on_changed_hints(client.team, hint.receiving_player)
|
||||
|
||||
elif cmd == 'StatusUpdate':
|
||||
update_client_status(ctx, client, args["status"])
|
||||
|
||||
@@ -2143,9 +2238,9 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||
hints = []
|
||||
for item_name_from_group in self.ctx.item_name_groups[game][item]:
|
||||
if item_name_from_group in self.ctx.item_names_for_game(game): # ensure item has an ID
|
||||
hints.extend(collect_hints(self.ctx, team, slot, item_name_from_group))
|
||||
hints.extend(collect_hints(self.ctx, team, slot, item_name_from_group, HintStatus.HINT_PRIORITY))
|
||||
else: # item name or id
|
||||
hints = collect_hints(self.ctx, team, slot, item)
|
||||
hints = collect_hints(self.ctx, team, slot, item, HintStatus.HINT_PRIORITY)
|
||||
|
||||
if hints:
|
||||
self.ctx.notify_hints(team, hints)
|
||||
@@ -2179,14 +2274,17 @@ class ServerCommandProcessor(CommonCommandProcessor):
|
||||
|
||||
if usable:
|
||||
if isinstance(location, int):
|
||||
hints = collect_hint_location_id(self.ctx, team, slot, location)
|
||||
hints = collect_hint_location_id(self.ctx, team, slot, location,
|
||||
HintStatus.HINT_UNSPECIFIED)
|
||||
elif game in self.ctx.location_name_groups and location in self.ctx.location_name_groups[game]:
|
||||
hints = []
|
||||
for loc_name_from_group in self.ctx.location_name_groups[game][location]:
|
||||
if loc_name_from_group in self.ctx.location_names_for_game(game):
|
||||
hints.extend(collect_hint_location_name(self.ctx, team, slot, loc_name_from_group))
|
||||
hints.extend(collect_hint_location_name(self.ctx, team, slot, loc_name_from_group,
|
||||
HintStatus.HINT_UNSPECIFIED))
|
||||
else:
|
||||
hints = collect_hint_location_name(self.ctx, team, slot, location)
|
||||
hints = collect_hint_location_name(self.ctx, team, slot, location,
|
||||
HintStatus.HINT_UNSPECIFIED)
|
||||
if hints:
|
||||
self.ctx.notify_hints(team, hints)
|
||||
else:
|
||||
|
||||
41
NetUtils.py
41
NetUtils.py
@@ -29,6 +29,14 @@ class ClientStatus(ByValue, enum.IntEnum):
|
||||
CLIENT_GOAL = 30
|
||||
|
||||
|
||||
class HintStatus(enum.IntEnum):
|
||||
HINT_FOUND = 0
|
||||
HINT_UNSPECIFIED = 1
|
||||
HINT_NO_PRIORITY = 10
|
||||
HINT_AVOID = 20
|
||||
HINT_PRIORITY = 30
|
||||
|
||||
|
||||
class SlotType(ByValue, enum.IntFlag):
|
||||
spectator = 0b00
|
||||
player = 0b01
|
||||
@@ -297,6 +305,20 @@ def add_json_location(parts: list, location_id: int, player: int = 0, **kwargs)
|
||||
parts.append({"text": str(location_id), "player": player, "type": JSONTypes.location_id, **kwargs})
|
||||
|
||||
|
||||
status_names: typing.Dict[HintStatus, str] = {
|
||||
HintStatus.HINT_FOUND: "(found)",
|
||||
HintStatus.HINT_UNSPECIFIED: "(unspecified)",
|
||||
HintStatus.HINT_NO_PRIORITY: "(no priority)",
|
||||
HintStatus.HINT_AVOID: "(avoid)",
|
||||
HintStatus.HINT_PRIORITY: "(priority)",
|
||||
}
|
||||
status_colors: typing.Dict[HintStatus, str] = {
|
||||
HintStatus.HINT_FOUND: "green",
|
||||
HintStatus.HINT_UNSPECIFIED: "white",
|
||||
HintStatus.HINT_NO_PRIORITY: "slateblue",
|
||||
HintStatus.HINT_AVOID: "salmon",
|
||||
HintStatus.HINT_PRIORITY: "plum",
|
||||
}
|
||||
class Hint(typing.NamedTuple):
|
||||
receiving_player: int
|
||||
finding_player: int
|
||||
@@ -305,14 +327,21 @@ class Hint(typing.NamedTuple):
|
||||
found: bool
|
||||
entrance: str = ""
|
||||
item_flags: int = 0
|
||||
status: HintStatus = HintStatus.HINT_UNSPECIFIED
|
||||
|
||||
def re_check(self, ctx, team) -> Hint:
|
||||
if self.found:
|
||||
if self.found and self.status == HintStatus.HINT_FOUND:
|
||||
return self
|
||||
found = self.location in ctx.location_checks[team, self.finding_player]
|
||||
if found:
|
||||
return Hint(self.receiving_player, self.finding_player, self.location, self.item, found, self.entrance,
|
||||
self.item_flags)
|
||||
return self._replace(found=found, status=HintStatus.HINT_FOUND)
|
||||
return self
|
||||
|
||||
def re_prioritize(self, ctx, status: HintStatus) -> Hint:
|
||||
if self.found and status != HintStatus.HINT_FOUND:
|
||||
status = HintStatus.HINT_FOUND
|
||||
if status != self.status:
|
||||
return self._replace(status=status)
|
||||
return self
|
||||
|
||||
def __hash__(self):
|
||||
@@ -334,10 +363,8 @@ class Hint(typing.NamedTuple):
|
||||
else:
|
||||
add_json_text(parts, "'s World")
|
||||
add_json_text(parts, ". ")
|
||||
if self.found:
|
||||
add_json_text(parts, "(found)", type="color", color="green")
|
||||
else:
|
||||
add_json_text(parts, "(not found)", type="color", color="red")
|
||||
add_json_text(parts, status_names.get(self.status, "(unknown)"), type="color",
|
||||
color=status_colors.get(self.status, "red"))
|
||||
|
||||
return {"cmd": "PrintJSON", "data": parts, "type": "Hint",
|
||||
"receiving": self.receiving_player,
|
||||
|
||||
@@ -828,7 +828,10 @@ class VerifyKeys(metaclass=FreezeValidKeys):
|
||||
f"is not a valid location name from {world.game}. "
|
||||
f"Did you mean '{picks[0][0]}' ({picks[0][1]}% sure)")
|
||||
|
||||
def __iter__(self) -> typing.Iterator[typing.Any]:
|
||||
return self.value.__iter__()
|
||||
|
||||
|
||||
class OptionDict(Option[typing.Dict[str, typing.Any]], VerifyKeys, typing.Mapping[str, typing.Any]):
|
||||
default = {}
|
||||
supports_weighting = False
|
||||
|
||||
@@ -76,6 +76,7 @@ Currently, the following games are supported:
|
||||
* Kingdom Hearts 1
|
||||
* Mega Man 2
|
||||
* Yacht Dice
|
||||
* Faxanadu
|
||||
|
||||
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
|
||||
|
||||
18
Utils.py
18
Utils.py
@@ -421,7 +421,8 @@ class RestrictedUnpickler(pickle.Unpickler):
|
||||
if module == "builtins" and name in safe_builtins:
|
||||
return getattr(builtins, name)
|
||||
# used by MultiServer -> savegame/multidata
|
||||
if module == "NetUtils" and name in {"NetworkItem", "ClientStatus", "Hint", "SlotType", "NetworkSlot"}:
|
||||
if module == "NetUtils" and name in {"NetworkItem", "ClientStatus", "Hint",
|
||||
"SlotType", "NetworkSlot", "HintStatus"}:
|
||||
return getattr(self.net_utils_module, name)
|
||||
# Options and Plando are unpickled by WebHost -> Generate
|
||||
if module == "worlds.generic" and name == "PlandoItem":
|
||||
@@ -481,7 +482,7 @@ def get_text_after(text: str, start: str) -> str:
|
||||
return text[text.index(start) + len(start):]
|
||||
|
||||
|
||||
loglevel_mapping = {'error': logging.ERROR, 'info': logging.INFO, 'warning': logging.WARNING, 'debug': logging.DEBUG}
|
||||
loglevel_mapping: dict[str, int] = {name.lower(): level for name, level in logging.getLevelNamesMapping().items()}
|
||||
|
||||
|
||||
def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, write_mode: str = "w",
|
||||
@@ -514,10 +515,13 @@ def init_logging(name: str, loglevel: typing.Union[str, int] = logging.INFO, wri
|
||||
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))
|
||||
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)
|
||||
root_logger.addHandler(stream_handler)
|
||||
|
||||
# Relay unhandled exceptions to logger.
|
||||
@@ -854,11 +858,10 @@ def async_start(co: Coroutine[None, None, typing.Any], name: Optional[str] = Non
|
||||
task.add_done_callback(_faf_tasks.discard)
|
||||
|
||||
|
||||
def deprecate(message: str):
|
||||
def deprecate(message: str, add_stacklevels: int = 0):
|
||||
if __debug__:
|
||||
raise Exception(message)
|
||||
import warnings
|
||||
warnings.warn(message)
|
||||
warnings.warn(message, stacklevel=2 + add_stacklevels)
|
||||
|
||||
|
||||
class DeprecateDict(dict):
|
||||
@@ -872,10 +875,9 @@ class DeprecateDict(dict):
|
||||
|
||||
def __getitem__(self, item: Any) -> Any:
|
||||
if self.should_error:
|
||||
deprecate(self.log_message)
|
||||
deprecate(self.log_message, add_stacklevels=1)
|
||||
elif __debug__:
|
||||
import warnings
|
||||
warnings.warn(self.log_message)
|
||||
warnings.warn(self.log_message, stacklevel=2)
|
||||
return super().__getitem__(item)
|
||||
|
||||
|
||||
|
||||
@@ -53,7 +53,7 @@
|
||||
<table class="range-rows" data-option="{{ option_name }}">
|
||||
<tbody>
|
||||
{{ RangeRow(option_name, option, option.range_start, option.range_start, True) }}
|
||||
{% if option.range_start < option.default < option.range_end %}
|
||||
{% if option.default is number and option.range_start < option.default < option.range_end %}
|
||||
{{ RangeRow(option_name, option, option.default, option.default, True) }}
|
||||
{% endif %}
|
||||
{{ RangeRow(option_name, option, option.range_end, option.range_end, True) }}
|
||||
|
||||
@@ -59,7 +59,7 @@
|
||||
finding_text: "Finding Player"
|
||||
location_text: "Location"
|
||||
entrance_text: "Entrance"
|
||||
found_text: "Found?"
|
||||
status_text: "Status"
|
||||
TooltipLabel:
|
||||
id: receiving
|
||||
sort_key: 'receiving'
|
||||
@@ -96,9 +96,9 @@
|
||||
valign: 'center'
|
||||
pos_hint: {"center_y": 0.5}
|
||||
TooltipLabel:
|
||||
id: found
|
||||
sort_key: 'found'
|
||||
text: root.found_text
|
||||
id: status
|
||||
sort_key: 'status'
|
||||
text: root.status_text
|
||||
halign: 'center'
|
||||
valign: 'center'
|
||||
pos_hint: {"center_y": 0.5}
|
||||
|
||||
@@ -55,19 +55,22 @@
|
||||
/worlds/dlcquest/ @axe-y @agilbert1412
|
||||
|
||||
# DOOM 1993
|
||||
/worlds/doom_1993/ @Daivuk
|
||||
/worlds/doom_1993/ @Daivuk @KScl
|
||||
|
||||
# DOOM II
|
||||
/worlds/doom_ii/ @Daivuk
|
||||
/worlds/doom_ii/ @Daivuk @KScl
|
||||
|
||||
# Factorio
|
||||
/worlds/factorio/ @Berserker66
|
||||
|
||||
# Faxanadu
|
||||
/worlds/faxanadu/ @Daivuk
|
||||
|
||||
# Final Fantasy Mystic Quest
|
||||
/worlds/ffmq/ @Alchav @wildham0
|
||||
|
||||
# Heretic
|
||||
/worlds/heretic/ @Daivuk
|
||||
/worlds/heretic/ @Daivuk @KScl
|
||||
|
||||
# Hollow Knight
|
||||
/worlds/hk/ @BadMagic100 @qwint
|
||||
|
||||
@@ -272,6 +272,7 @@ These packets are sent purely from client to server. They are not accepted by cl
|
||||
* [Sync](#Sync)
|
||||
* [LocationChecks](#LocationChecks)
|
||||
* [LocationScouts](#LocationScouts)
|
||||
* [UpdateHint](#UpdateHint)
|
||||
* [StatusUpdate](#StatusUpdate)
|
||||
* [Say](#Say)
|
||||
* [GetDataPackage](#GetDataPackage)
|
||||
@@ -342,6 +343,29 @@ This is useful in cases where an item appears in the game world, such as 'ledge
|
||||
| locations | list\[int\] | The ids of the locations seen by the client. May contain any number of locations, even ones sent before; duplicates do not cause issues with the Archipelago server. |
|
||||
| create_as_hint | int | If non-zero, the scouted locations get created and broadcasted as a player-visible hint. <br/>If 2 only new hints are broadcast, however this does not remove them from the LocationInfo reply. |
|
||||
|
||||
### UpdateHint
|
||||
Sent to the server to update the status of a Hint. The client must be the 'receiving_player' of the Hint, or the update fails.
|
||||
|
||||
### Arguments
|
||||
| Name | Type | Notes |
|
||||
| ---- | ---- | ----- |
|
||||
| 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. |
|
||||
|
||||
#### HintStatus
|
||||
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
|
||||
```
|
||||
|
||||
### 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)
|
||||
|
||||
|
||||
@@ -288,8 +288,8 @@ like entrance randomization in logic.
|
||||
|
||||
Regions have a list called `exits`, containing `Entrance` objects representing transitions to other regions.
|
||||
|
||||
There must be one special region, "Menu", from which the logic unfolds. AP assumes that a player will always be able to
|
||||
return to the "Menu" region by resetting the game ("Save and quit").
|
||||
There must be one special region (Called "Menu" by default, but configurable using [origin_region_name](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L295-L296)),
|
||||
from which the logic unfolds. AP assumes that a player will always be able to return to this starting region by resetting the game ("Save and quit").
|
||||
|
||||
### Entrances
|
||||
|
||||
@@ -328,6 +328,9 @@ Even doing `state.can_reach_location` or `state.can_reach_entrance` is problemat
|
||||
You can use `multiworld.register_indirect_condition(region, entrance)` to explicitly tell the generator that, when a given region becomes accessible, it is necessary to re-check a specific entrance.
|
||||
You **must** use `multiworld.register_indirect_condition` if you perform this kind of `can_reach` from an entrance access rule, unless you have a **very** good technical understanding of the relevant code and can reason why it will never lead to problems in your case.
|
||||
|
||||
Alternatively, you can set [world.explicit_indirect_conditions = False](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/AutoWorld.py#L298-L301),
|
||||
avoiding the need for indirect conditions at the expense of performance.
|
||||
|
||||
### Item Rules
|
||||
|
||||
An item rule is a function that returns `True` or `False` for a `Location` based on a single item. It can be used to
|
||||
@@ -463,7 +466,7 @@ The world has to provide the following things for generation:
|
||||
|
||||
* the properties mentioned above
|
||||
* additions to the item pool
|
||||
* additions to the regions list: at least one called "Menu"
|
||||
* additions to the regions list: at least one named after the world class's origin_region_name ("Menu" by default)
|
||||
* locations placed inside those regions
|
||||
* a `def create_item(self, item: str) -> MyGameItem` to create any item on demand
|
||||
* applying `self.multiworld.push_precollected` for world-defined start inventory
|
||||
@@ -516,7 +519,7 @@ def generate_early(self) -> None:
|
||||
|
||||
```python
|
||||
def create_regions(self) -> None:
|
||||
# Add regions to the multiworld. "Menu" is the required starting point.
|
||||
# Add regions to the multiworld. One of them must use the origin_region_name as its name ("Menu" by default).
|
||||
# Arguments to Region() are name, player, multiworld, and optionally hint_text
|
||||
menu_region = Region("Menu", self.player, self.multiworld)
|
||||
self.multiworld.regions.append(menu_region) # or use += [menu_region...]
|
||||
|
||||
96
kvui.py
96
kvui.py
@@ -52,6 +52,7 @@ from kivy.uix.boxlayout import BoxLayout
|
||||
from kivy.uix.floatlayout import FloatLayout
|
||||
from kivy.uix.label import Label
|
||||
from kivy.uix.progressbar import ProgressBar
|
||||
from kivy.uix.dropdown import DropDown
|
||||
from kivy.utils import escape_markup
|
||||
from kivy.lang import Builder
|
||||
from kivy.uix.recycleview.views import RecycleDataViewBehavior
|
||||
@@ -63,7 +64,7 @@ from kivy.uix.popup import Popup
|
||||
|
||||
fade_in_animation = Animation(opacity=0, duration=0) + Animation(opacity=1, duration=0.25)
|
||||
|
||||
from NetUtils import JSONtoTextParser, JSONMessagePart, SlotType
|
||||
from NetUtils import JSONtoTextParser, JSONMessagePart, SlotType, HintStatus
|
||||
from Utils import async_start, get_input_text_from_response
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
@@ -300,11 +301,11 @@ class SelectableLabel(RecycleDataViewBehavior, TooltipLabel):
|
||||
""" Respond to the selection of items in the view. """
|
||||
self.selected = is_selected
|
||||
|
||||
|
||||
class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
||||
selected = BooleanProperty(False)
|
||||
striped = BooleanProperty(False)
|
||||
index = None
|
||||
dropdown: DropDown
|
||||
|
||||
def __init__(self):
|
||||
super(HintLabel, self).__init__()
|
||||
@@ -313,10 +314,32 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
||||
self.finding_text = ""
|
||||
self.location_text = ""
|
||||
self.entrance_text = ""
|
||||
self.found_text = ""
|
||||
self.status_text = ""
|
||||
self.hint = {}
|
||||
for child in self.children:
|
||||
child.bind(texture_size=self.set_height)
|
||||
|
||||
|
||||
ctx = App.get_running_app().ctx
|
||||
self.dropdown = DropDown()
|
||||
|
||||
def set_value(button):
|
||||
self.dropdown.select(button.status)
|
||||
|
||||
def select(instance, data):
|
||||
ctx.update_hint(self.hint["location"],
|
||||
self.hint["finding_player"],
|
||||
data)
|
||||
|
||||
for status in (HintStatus.HINT_NO_PRIORITY, HintStatus.HINT_PRIORITY, HintStatus.HINT_AVOID):
|
||||
name = status_names[status]
|
||||
status_button = Button(text=name, size_hint_y=None, height=dp(50))
|
||||
status_button.status = status
|
||||
status_button.bind(on_release=set_value)
|
||||
self.dropdown.add_widget(status_button)
|
||||
|
||||
self.dropdown.bind(on_select=select)
|
||||
|
||||
def set_height(self, instance, value):
|
||||
self.height = max([child.texture_size[1] for child in self.children])
|
||||
|
||||
@@ -328,7 +351,8 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
||||
self.finding_text = data["finding"]["text"]
|
||||
self.location_text = data["location"]["text"]
|
||||
self.entrance_text = data["entrance"]["text"]
|
||||
self.found_text = data["found"]["text"]
|
||||
self.status_text = data["status"]["text"]
|
||||
self.hint = data["status"]["hint"]
|
||||
self.height = self.minimum_height
|
||||
return super(HintLabel, self).refresh_view_attrs(rv, index, data)
|
||||
|
||||
@@ -338,13 +362,21 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
||||
return True
|
||||
if self.index: # skip header
|
||||
if self.collide_point(*touch.pos):
|
||||
if self.selected:
|
||||
status_label = self.ids["status"]
|
||||
if status_label.collide_point(*touch.pos):
|
||||
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
|
||||
# open a dropdown
|
||||
self.dropdown.open(self.ids["status"])
|
||||
elif self.selected:
|
||||
self.parent.clear_selection()
|
||||
else:
|
||||
text = "".join((self.receiving_text, "\'s ", self.item_text, " is at ", self.location_text, " in ",
|
||||
self.finding_text, "\'s World", (" at " + self.entrance_text)
|
||||
if self.entrance_text != "Vanilla"
|
||||
else "", ". (", self.found_text.lower(), ")"))
|
||||
else "", ". (", self.status_text.lower(), ")"))
|
||||
temp = MarkupLabel(text).markup
|
||||
text = "".join(
|
||||
part for part in temp if not part.startswith(("[color", "[/color]", "[ref=", "[/ref]")))
|
||||
@@ -358,18 +390,16 @@ class HintLabel(RecycleDataViewBehavior, BoxLayout):
|
||||
for child in self.children:
|
||||
if child.collide_point(*touch.pos):
|
||||
key = child.sort_key
|
||||
parent.hint_sorter = lambda element: remove_between_brackets.sub("", element[key]["text"]).lower()
|
||||
if key == "status":
|
||||
parent.hint_sorter = lambda element: element["status"]["hint"]["status"]
|
||||
else: parent.hint_sorter = lambda element: remove_between_brackets.sub("", element[key]["text"]).lower()
|
||||
if key == parent.sort_key:
|
||||
# second click reverses order
|
||||
parent.reversed = not parent.reversed
|
||||
else:
|
||||
parent.sort_key = key
|
||||
parent.reversed = False
|
||||
break
|
||||
else:
|
||||
logging.warning("Did not find clicked header for sorting.")
|
||||
|
||||
App.get_running_app().update_hints()
|
||||
App.get_running_app().update_hints()
|
||||
|
||||
def apply_selection(self, rv, index, is_selected):
|
||||
""" Respond to the selection of items in the view. """
|
||||
@@ -663,7 +693,7 @@ class GameManager(App):
|
||||
self.energy_link_label.text = f"EL: {Utils.format_SI_prefix(self.ctx.current_energy_link_value)}J"
|
||||
|
||||
def update_hints(self):
|
||||
hints = self.ctx.stored_data[f"_read_hints_{self.ctx.team}_{self.ctx.slot}"]
|
||||
hints = self.ctx.stored_data.get(f"_read_hints_{self.ctx.team}_{self.ctx.slot}", [])
|
||||
self.log_panels["Hints"].refresh_hints(hints)
|
||||
|
||||
# default F1 keybind, opens a settings menu, that seems to break the layout engine once closed
|
||||
@@ -719,6 +749,22 @@ class UILog(RecycleView):
|
||||
element.height = element.texture_size[1]
|
||||
|
||||
|
||||
status_names: typing.Dict[HintStatus, str] = {
|
||||
HintStatus.HINT_FOUND: "Found",
|
||||
HintStatus.HINT_UNSPECIFIED: "Unspecified",
|
||||
HintStatus.HINT_NO_PRIORITY: "No Priority",
|
||||
HintStatus.HINT_AVOID: "Avoid",
|
||||
HintStatus.HINT_PRIORITY: "Priority",
|
||||
}
|
||||
status_colors: typing.Dict[HintStatus, str] = {
|
||||
HintStatus.HINT_FOUND: "green",
|
||||
HintStatus.HINT_UNSPECIFIED: "white",
|
||||
HintStatus.HINT_NO_PRIORITY: "cyan",
|
||||
HintStatus.HINT_AVOID: "salmon",
|
||||
HintStatus.HINT_PRIORITY: "plum",
|
||||
}
|
||||
|
||||
|
||||
class HintLog(RecycleView):
|
||||
header = {
|
||||
"receiving": {"text": "[u]Receiving Player[/u]"},
|
||||
@@ -726,12 +772,13 @@ class HintLog(RecycleView):
|
||||
"finding": {"text": "[u]Finding Player[/u]"},
|
||||
"location": {"text": "[u]Location[/u]"},
|
||||
"entrance": {"text": "[u]Entrance[/u]"},
|
||||
"found": {"text": "[u]Status[/u]"},
|
||||
"status": {"text": "[u]Status[/u]",
|
||||
"hint": {"receiving_player": -1, "location": -1, "finding_player": -1, "status": ""}},
|
||||
"striped": True,
|
||||
}
|
||||
|
||||
sort_key: str = ""
|
||||
reversed: bool = False
|
||||
reversed: bool = True
|
||||
|
||||
def __init__(self, parser):
|
||||
super(HintLog, self).__init__()
|
||||
@@ -739,8 +786,18 @@ class HintLog(RecycleView):
|
||||
self.parser = parser
|
||||
|
||||
def refresh_hints(self, hints):
|
||||
if not hints: # Fix the scrolling looking visually wrong in some edge cases
|
||||
self.scroll_y = 1.0
|
||||
data = []
|
||||
ctx = App.get_running_app().ctx
|
||||
for hint in hints:
|
||||
if not hint.get("status"): # Allows connecting to old servers
|
||||
hint["status"] = HintStatus.HINT_FOUND if hint["found"] else HintStatus.HINT_UNSPECIFIED
|
||||
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:
|
||||
hint_status_node = f"[u]{hint_status_node}[/u]"
|
||||
data.append({
|
||||
"receiving": {"text": self.parser.handle_node({"type": "player_id", "text": hint["receiving_player"]})},
|
||||
"item": {"text": self.parser.handle_node({
|
||||
@@ -758,9 +815,10 @@ class HintLog(RecycleView):
|
||||
"entrance": {"text": self.parser.handle_node({"type": "color" if hint["entrance"] else "text",
|
||||
"color": "blue", "text": hint["entrance"]
|
||||
if hint["entrance"] else "Vanilla"})},
|
||||
"found": {
|
||||
"text": self.parser.handle_node({"type": "color", "color": "green" if hint["found"] else "red",
|
||||
"text": "Found" if hint["found"] else "Not Found"})},
|
||||
"status": {
|
||||
"text": hint_status_node,
|
||||
"hint": hint,
|
||||
},
|
||||
})
|
||||
|
||||
data.sort(key=self.hint_sorter, reverse=self.reversed)
|
||||
@@ -771,7 +829,7 @@ class HintLog(RecycleView):
|
||||
|
||||
@staticmethod
|
||||
def hint_sorter(element: dict) -> str:
|
||||
return ""
|
||||
return element["status"]["hint"]["status"] # By status by default
|
||||
|
||||
def fix_heights(self):
|
||||
"""Workaround fix for divergent texture and layout heights"""
|
||||
|
||||
10
settings.py
10
settings.py
@@ -7,6 +7,7 @@ import os
|
||||
import os.path
|
||||
import shutil
|
||||
import sys
|
||||
import types
|
||||
import typing
|
||||
import warnings
|
||||
from enum import IntEnum
|
||||
@@ -162,8 +163,13 @@ class Group:
|
||||
else:
|
||||
# assign value, try to upcast to type hint
|
||||
annotation = self.get_type_hints().get(k, None)
|
||||
candidates = [] if annotation is None else \
|
||||
typing.get_args(annotation) if typing.get_origin(annotation) is Union else [annotation]
|
||||
candidates = (
|
||||
[] if annotation is None else (
|
||||
typing.get_args(annotation)
|
||||
if typing.get_origin(annotation) in (Union, types.UnionType)
|
||||
else [annotation]
|
||||
)
|
||||
)
|
||||
none_type = type(None)
|
||||
for cls in candidates:
|
||||
assert isinstance(cls, type), f"{self.__class__.__name__}.{k}: type {cls} not supported in settings"
|
||||
|
||||
2
setup.py
2
setup.py
@@ -321,7 +321,7 @@ class BuildExeCommand(cx_Freeze.command.build_exe.build_exe):
|
||||
f"{ex}\nPlease close all AP instances and delete manually.")
|
||||
|
||||
# regular cx build
|
||||
self.buildtime = datetime.datetime.utcnow()
|
||||
self.buildtime = datetime.datetime.now(datetime.timezone.utc)
|
||||
super().run()
|
||||
|
||||
# manually copy built modules to lib folder. cx_Freeze does not know they exist.
|
||||
|
||||
@@ -80,3 +80,21 @@ class TestBase(unittest.TestCase):
|
||||
call_all(multiworld, step)
|
||||
self.assertEqual(created_items, multiworld.itempool,
|
||||
f"{game_name} modified the itempool during {step}")
|
||||
|
||||
def test_locality_not_modified(self):
|
||||
"""Test that worlds don't modify the locality of items after duplicates are resolved"""
|
||||
gen_steps = ("generate_early", "create_regions", "create_items")
|
||||
additional_steps = ("set_rules", "generate_basic", "pre_fill")
|
||||
worlds_to_test = {game: world for game, world in AutoWorldRegister.world_types.items()}
|
||||
for game_name, world_type in worlds_to_test.items():
|
||||
with self.subTest("Game", game=game_name):
|
||||
multiworld = setup_solo_multiworld(world_type, gen_steps)
|
||||
local_items = multiworld.worlds[1].options.local_items.value.copy()
|
||||
non_local_items = multiworld.worlds[1].options.non_local_items.value.copy()
|
||||
for step in additional_steps:
|
||||
with self.subTest("step", step=step):
|
||||
call_all(multiworld, step)
|
||||
self.assertEqual(local_items, multiworld.worlds[1].options.local_items.value,
|
||||
f"{game_name} modified local_items during {step}")
|
||||
self.assertEqual(non_local_items, multiworld.worlds[1].options.non_local_items.value,
|
||||
f"{game_name} modified non_local_items during {step}")
|
||||
|
||||
16
test/general/test_settings.py
Normal file
16
test/general/test_settings.py
Normal file
@@ -0,0 +1,16 @@
|
||||
from unittest import TestCase
|
||||
|
||||
from settings import Group
|
||||
from worlds.AutoWorld import AutoWorldRegister
|
||||
|
||||
|
||||
class TestSettings(TestCase):
|
||||
def test_settings_can_update(self) -> None:
|
||||
"""
|
||||
Test that world settings can update.
|
||||
"""
|
||||
for game_name, world_type in AutoWorldRegister.world_types.items():
|
||||
with self.subTest(game=game_name):
|
||||
if world_type.settings is not None:
|
||||
assert isinstance(world_type.settings, Group)
|
||||
world_type.settings.update({}) # a previous bug had a crash in this call to update
|
||||
@@ -33,7 +33,10 @@ class AutoWorldRegister(type):
|
||||
# lazy loading + caching to minimize runtime cost
|
||||
if cls.__settings is None:
|
||||
from settings import get_settings
|
||||
cls.__settings = get_settings()[cls.settings_key]
|
||||
try:
|
||||
cls.__settings = get_settings()[cls.settings_key]
|
||||
except AttributeError:
|
||||
return None
|
||||
return cls.__settings
|
||||
|
||||
def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> AutoWorldRegister:
|
||||
|
||||
@@ -103,7 +103,7 @@ def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, path
|
||||
try:
|
||||
import zipfile
|
||||
zip = zipfile.ZipFile(apworld_path)
|
||||
directories = [f.filename.strip('/') for f in zip.filelist if f.CRC == 0 and f.file_size == 0 and f.filename.count('/') == 1]
|
||||
directories = [f.name for f in zipfile.Path(zip).iterdir() if f.is_dir()]
|
||||
if len(directories) == 1 and directories[0] in apworld_path.stem:
|
||||
module_name = directories[0]
|
||||
apworld_name = module_name + ".apworld"
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import typing
|
||||
from dataclasses import dataclass
|
||||
|
||||
from BaseClasses import MultiWorld
|
||||
from Options import Choice, Range, DeathLink, DefaultOnToggle, FreeText, ItemsAccessibility, Option, \
|
||||
from Options import Choice, Range, DeathLink, DefaultOnToggle, FreeText, ItemsAccessibility, PerGameCommonOptions, \
|
||||
PlandoBosses, PlandoConnections, PlandoTexts, Removed, StartInventoryPool, Toggle
|
||||
from .EntranceShuffle import default_connections, default_dungeon_connections, \
|
||||
inverted_default_connections, inverted_default_dungeon_connections
|
||||
@@ -742,86 +742,86 @@ class ALttPPlandoTexts(PlandoTexts):
|
||||
valid_keys = TextTable.valid_keys
|
||||
|
||||
|
||||
alttp_options: typing.Dict[str, type(Option)] = {
|
||||
"accessibility": ItemsAccessibility,
|
||||
"plando_connections": ALttPPlandoConnections,
|
||||
"plando_texts": ALttPPlandoTexts,
|
||||
"start_inventory_from_pool": StartInventoryPool,
|
||||
"goal": Goal,
|
||||
"mode": Mode,
|
||||
"glitches_required": GlitchesRequired,
|
||||
"dark_room_logic": DarkRoomLogic,
|
||||
"open_pyramid": OpenPyramid,
|
||||
"crystals_needed_for_gt": CrystalsTower,
|
||||
"crystals_needed_for_ganon": CrystalsGanon,
|
||||
"triforce_pieces_mode": TriforcePiecesMode,
|
||||
"triforce_pieces_percentage": TriforcePiecesPercentage,
|
||||
"triforce_pieces_required": TriforcePiecesRequired,
|
||||
"triforce_pieces_available": TriforcePiecesAvailable,
|
||||
"triforce_pieces_extra": TriforcePiecesExtra,
|
||||
"entrance_shuffle": EntranceShuffle,
|
||||
"entrance_shuffle_seed": EntranceShuffleSeed,
|
||||
"big_key_shuffle": big_key_shuffle,
|
||||
"small_key_shuffle": small_key_shuffle,
|
||||
"key_drop_shuffle": key_drop_shuffle,
|
||||
"compass_shuffle": compass_shuffle,
|
||||
"map_shuffle": map_shuffle,
|
||||
"restrict_dungeon_item_on_boss": RestrictBossItem,
|
||||
"item_pool": ItemPool,
|
||||
"item_functionality": ItemFunctionality,
|
||||
"enemy_health": EnemyHealth,
|
||||
"enemy_damage": EnemyDamage,
|
||||
"progressive": Progressive,
|
||||
"swordless": Swordless,
|
||||
"dungeon_counters": DungeonCounters,
|
||||
"retro_bow": RetroBow,
|
||||
"retro_caves": RetroCaves,
|
||||
"hints": Hints,
|
||||
"scams": Scams,
|
||||
"boss_shuffle": LTTPBosses,
|
||||
"pot_shuffle": PotShuffle,
|
||||
"enemy_shuffle": EnemyShuffle,
|
||||
"killable_thieves": KillableThieves,
|
||||
"bush_shuffle": BushShuffle,
|
||||
"shop_item_slots": ShopItemSlots,
|
||||
"randomize_shop_inventories": RandomizeShopInventories,
|
||||
"shuffle_shop_inventories": ShuffleShopInventories,
|
||||
"include_witch_hut": IncludeWitchHut,
|
||||
"randomize_shop_prices": RandomizeShopPrices,
|
||||
"randomize_cost_types": RandomizeCostTypes,
|
||||
"shop_price_modifier": ShopPriceModifier,
|
||||
"shuffle_capacity_upgrades": ShuffleCapacityUpgrades,
|
||||
"bombless_start": BomblessStart,
|
||||
"shuffle_prizes": ShufflePrizes,
|
||||
"tile_shuffle": TileShuffle,
|
||||
"misery_mire_medallion": MiseryMireMedallion,
|
||||
"turtle_rock_medallion": TurtleRockMedallion,
|
||||
"glitch_boots": GlitchBoots,
|
||||
"beemizer_total_chance": BeemizerTotalChance,
|
||||
"beemizer_trap_chance": BeemizerTrapChance,
|
||||
"timer": Timer,
|
||||
"countdown_start_time": CountdownStartTime,
|
||||
"red_clock_time": RedClockTime,
|
||||
"blue_clock_time": BlueClockTime,
|
||||
"green_clock_time": GreenClockTime,
|
||||
"death_link": DeathLink,
|
||||
"allow_collect": AllowCollect,
|
||||
"ow_palettes": OWPalette,
|
||||
"uw_palettes": UWPalette,
|
||||
"hud_palettes": HUDPalette,
|
||||
"sword_palettes": SwordPalette,
|
||||
"shield_palettes": ShieldPalette,
|
||||
# "link_palettes": LinkPalette,
|
||||
"heartbeep": HeartBeep,
|
||||
"heartcolor": HeartColor,
|
||||
"quickswap": QuickSwap,
|
||||
"menuspeed": MenuSpeed,
|
||||
"music": Music,
|
||||
"reduceflashing": ReduceFlashing,
|
||||
"triforcehud": TriforceHud,
|
||||
@dataclass
|
||||
class ALTTPOptions(PerGameCommonOptions):
|
||||
accessibility: ItemsAccessibility
|
||||
plando_connections: ALttPPlandoConnections
|
||||
plando_texts: ALttPPlandoTexts
|
||||
start_inventory_from_pool: StartInventoryPool
|
||||
goal: Goal
|
||||
mode: Mode
|
||||
glitches_required: GlitchesRequired
|
||||
dark_room_logic: DarkRoomLogic
|
||||
open_pyramid: OpenPyramid
|
||||
crystals_needed_for_gt: CrystalsTower
|
||||
crystals_needed_for_ganon: CrystalsGanon
|
||||
triforce_pieces_mode: TriforcePiecesMode
|
||||
triforce_pieces_percentage: TriforcePiecesPercentage
|
||||
triforce_pieces_required: TriforcePiecesRequired
|
||||
triforce_pieces_available: TriforcePiecesAvailable
|
||||
triforce_pieces_extra: TriforcePiecesExtra
|
||||
entrance_shuffle: EntranceShuffle
|
||||
entrance_shuffle_seed: EntranceShuffleSeed
|
||||
big_key_shuffle: big_key_shuffle
|
||||
small_key_shuffle: small_key_shuffle
|
||||
key_drop_shuffle: key_drop_shuffle
|
||||
compass_shuffle: compass_shuffle
|
||||
map_shuffle: map_shuffle
|
||||
restrict_dungeon_item_on_boss: RestrictBossItem
|
||||
item_pool: ItemPool
|
||||
item_functionality: ItemFunctionality
|
||||
enemy_health: EnemyHealth
|
||||
enemy_damage: EnemyDamage
|
||||
progressive: Progressive
|
||||
swordless: Swordless
|
||||
dungeon_counters: DungeonCounters
|
||||
retro_bow: RetroBow
|
||||
retro_caves: RetroCaves
|
||||
hints: Hints
|
||||
scams: Scams
|
||||
boss_shuffle: LTTPBosses
|
||||
pot_shuffle: PotShuffle
|
||||
enemy_shuffle: EnemyShuffle
|
||||
killable_thieves: KillableThieves
|
||||
bush_shuffle: BushShuffle
|
||||
shop_item_slots: ShopItemSlots
|
||||
randomize_shop_inventories: RandomizeShopInventories
|
||||
shuffle_shop_inventories: ShuffleShopInventories
|
||||
include_witch_hut: IncludeWitchHut
|
||||
randomize_shop_prices: RandomizeShopPrices
|
||||
randomize_cost_types: RandomizeCostTypes
|
||||
shop_price_modifier: ShopPriceModifier
|
||||
shuffle_capacity_upgrades: ShuffleCapacityUpgrades
|
||||
bombless_start: BomblessStart
|
||||
shuffle_prizes: ShufflePrizes
|
||||
tile_shuffle: TileShuffle
|
||||
misery_mire_medallion: MiseryMireMedallion
|
||||
turtle_rock_medallion: TurtleRockMedallion
|
||||
glitch_boots: GlitchBoots
|
||||
beemizer_total_chance: BeemizerTotalChance
|
||||
beemizer_trap_chance: BeemizerTrapChance
|
||||
timer: Timer
|
||||
countdown_start_time: CountdownStartTime
|
||||
red_clock_time: RedClockTime
|
||||
blue_clock_time: BlueClockTime
|
||||
green_clock_time: GreenClockTime
|
||||
death_link: DeathLink
|
||||
allow_collect: AllowCollect
|
||||
ow_palettes: OWPalette
|
||||
uw_palettes: UWPalette
|
||||
hud_palettes: HUDPalette
|
||||
sword_palettes: SwordPalette
|
||||
shield_palettes: ShieldPalette
|
||||
# link_palettes: LinkPalette
|
||||
heartbeep: HeartBeep
|
||||
heartcolor: HeartColor
|
||||
quickswap: QuickSwap
|
||||
menuspeed: MenuSpeed
|
||||
music: Music
|
||||
reduceflashing: ReduceFlashing
|
||||
triforcehud: TriforceHud
|
||||
|
||||
# removed:
|
||||
"goals": Removed,
|
||||
"smallkey_shuffle": Removed,
|
||||
"bigkey_shuffle": Removed,
|
||||
}
|
||||
goals: Removed
|
||||
smallkey_shuffle: Removed
|
||||
bigkey_shuffle: Removed
|
||||
|
||||
@@ -782,8 +782,8 @@ def get_nonnative_item_sprite(code: int) -> int:
|
||||
|
||||
|
||||
def patch_rom(world: MultiWorld, rom: LocalRom, player: int, enemized: bool):
|
||||
local_random = world.per_slot_randoms[player]
|
||||
local_world = world.worlds[player]
|
||||
local_random = local_world.random
|
||||
|
||||
# patch items
|
||||
|
||||
@@ -1867,7 +1867,7 @@ def apply_oof_sfx(rom, oof: str):
|
||||
def apply_rom_settings(rom, beep, color, quickswap, menuspeed, music: bool, sprite: str, oof: str, palettes_options,
|
||||
world=None, player=1, allow_random_on_event=False, reduceflashing=False,
|
||||
triforcehud: str = None, deathlink: bool = False, allowcollect: bool = False):
|
||||
local_random = random if not world else world.per_slot_randoms[player]
|
||||
local_random = random if not world else world.worlds[player].random
|
||||
disable_music: bool = not music
|
||||
# enable instant item menu
|
||||
if menuspeed == 'instant':
|
||||
@@ -2197,8 +2197,9 @@ def write_string_to_rom(rom, target, string):
|
||||
|
||||
def write_strings(rom, world, player):
|
||||
from . import ALTTPWorld
|
||||
local_random = world.per_slot_randoms[player]
|
||||
|
||||
w: ALTTPWorld = world.worlds[player]
|
||||
local_random = w.random
|
||||
|
||||
tt = TextTable()
|
||||
tt.removeUnwantedText()
|
||||
@@ -2425,7 +2426,7 @@ def write_strings(rom, world, player):
|
||||
if world.worlds[player].has_progressive_bows and (w.difficulty_requirements.progressive_bow_limit >= 2 or (
|
||||
world.swordless[player] or world.glitches_required[player] == 'no_glitches')):
|
||||
prog_bow_locs = world.find_item_locations('Progressive Bow', player, True)
|
||||
world.per_slot_randoms[player].shuffle(prog_bow_locs)
|
||||
local_random.shuffle(prog_bow_locs)
|
||||
found_bow = False
|
||||
found_bow_alt = False
|
||||
while prog_bow_locs and not (found_bow and found_bow_alt):
|
||||
|
||||
@@ -1,28 +1,27 @@
|
||||
import logging
|
||||
import os
|
||||
import random
|
||||
import settings
|
||||
import threading
|
||||
import typing
|
||||
|
||||
import Utils
|
||||
import settings
|
||||
from BaseClasses import Item, CollectionState, Tutorial, MultiWorld
|
||||
from worlds.AutoWorld import World, WebWorld, LogicMixin
|
||||
from .Client import ALTTPSNIClient
|
||||
from .Dungeons import create_dungeons, Dungeon
|
||||
from .EntranceShuffle import link_entrances, link_inverted_entrances, plando_connect
|
||||
from .InvertedRegions import create_inverted_regions, mark_dark_world_regions
|
||||
from .ItemPool import generate_itempool, difficulties
|
||||
from .Items import item_init_table, item_name_groups, item_table, GetBeemizerItem
|
||||
from .Options import alttp_options, small_key_shuffle
|
||||
from .Options import ALTTPOptions, small_key_shuffle
|
||||
from .Regions import lookup_name_to_id, create_regions, mark_light_world_regions, lookup_vanilla_location_to_entrance, \
|
||||
is_main_entrance, key_drop_data
|
||||
from .Client import ALTTPSNIClient
|
||||
from .Rom import LocalRom, patch_rom, patch_race_rom, check_enemizer, patch_enemizer, apply_rom_settings, \
|
||||
get_hash_string, get_base_rom_path, LttPDeltaPatch
|
||||
from .Rules import set_rules
|
||||
from .Shops import create_shops, Shop, push_shop_inventories, ShopType, price_rate_display, price_type_display_name
|
||||
from .SubClasses import ALttPItem, LTTPRegionType
|
||||
from worlds.AutoWorld import World, WebWorld, LogicMixin
|
||||
from .StateHelpers import can_buy_unlimited
|
||||
from .SubClasses import ALttPItem, LTTPRegionType
|
||||
|
||||
lttp_logger = logging.getLogger("A Link to the Past")
|
||||
|
||||
@@ -132,7 +131,8 @@ class ALTTPWorld(World):
|
||||
Ganon!
|
||||
"""
|
||||
game = "A Link to the Past"
|
||||
option_definitions = alttp_options
|
||||
options_dataclass = ALTTPOptions
|
||||
options: ALTTPOptions
|
||||
settings_key = "lttp_options"
|
||||
settings: typing.ClassVar[ALTTPSettings]
|
||||
topology_present = True
|
||||
@@ -286,13 +286,22 @@ class ALTTPWorld(World):
|
||||
if not os.path.exists(rom_file):
|
||||
raise FileNotFoundError(rom_file)
|
||||
if multiworld.is_race:
|
||||
import xxtea
|
||||
import xxtea # noqa
|
||||
for player in multiworld.get_game_players(cls.game):
|
||||
if multiworld.worlds[player].use_enemizer:
|
||||
check_enemizer(multiworld.worlds[player].enemizer_path)
|
||||
break
|
||||
|
||||
def generate_early(self):
|
||||
# write old options
|
||||
import dataclasses
|
||||
is_first = self.player == min(self.multiworld.get_game_players(self.game))
|
||||
|
||||
for field in dataclasses.fields(self.options_dataclass):
|
||||
if is_first:
|
||||
setattr(self.multiworld, field.name, {})
|
||||
getattr(self.multiworld, field.name)[self.player] = getattr(self.options, field.name)
|
||||
# end of old options re-establisher
|
||||
|
||||
player = self.player
|
||||
multiworld = self.multiworld
|
||||
@@ -536,12 +545,10 @@ class ALTTPWorld(World):
|
||||
|
||||
@property
|
||||
def use_enemizer(self) -> bool:
|
||||
world = self.multiworld
|
||||
player = self.player
|
||||
return bool(world.boss_shuffle[player] or world.enemy_shuffle[player]
|
||||
or world.enemy_health[player] != 'default' or world.enemy_damage[player] != 'default'
|
||||
or world.pot_shuffle[player] or world.bush_shuffle[player]
|
||||
or world.killable_thieves[player])
|
||||
return bool(self.options.boss_shuffle or self.options.enemy_shuffle
|
||||
or self.options.enemy_health != 'default' or self.options.enemy_damage != 'default'
|
||||
or self.options.pot_shuffle or self.options.bush_shuffle
|
||||
or self.options.killable_thieves)
|
||||
|
||||
def generate_output(self, output_directory: str):
|
||||
multiworld = self.multiworld
|
||||
|
||||
32
worlds/alttp/docs/fr_A Link to the Past.md
Normal file
32
worlds/alttp/docs/fr_A Link to the Past.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# A Link to the Past
|
||||
|
||||
## Où se trouve la page des paramètres ?
|
||||
|
||||
La [page des paramètres du joueur pour ce jeu](../player-options) contient tous les paramètres dont vous avez besoin
|
||||
pour configurer et exporter le fichier.
|
||||
|
||||
## Quel est l'effet de la randomisation sur ce jeu ?
|
||||
|
||||
Les objets que le joueur devrait normalement obtenir au cours du jeu ont été déplacés. Il y a tout de même une logique
|
||||
pour que le jeu puisse être terminé, mais dû au mélange des objets, le joueur peut avoir besoin d'accéder à certaines
|
||||
zones plus tôt que dans le jeu original.
|
||||
|
||||
## Quels sont les objets et endroits mélangés ?
|
||||
|
||||
Tous les objets principaux, les collectibles et munitions peuvent être mélangés, et tous les endroits qui
|
||||
pourraient contenir un de ces objets peuvent avoir leur contenu modifié.
|
||||
|
||||
## Quels objets peuvent être dans le monde d'un autre joueur ?
|
||||
|
||||
Un objet pouvant être mélangé peut être aussi placé dans le monde d'un autre joueur. Il est possible de limiter certains
|
||||
objets à votre propre monde.
|
||||
|
||||
## À quoi ressemble un objet d'un autre monde dans LttP ?
|
||||
|
||||
Les objets appartenant à d'autres mondes sont représentés par une Étoile de Super Mario World.
|
||||
|
||||
## Quand le joueur reçoit un objet, que ce passe-t-il ?
|
||||
|
||||
Quand le joueur reçoit un objet, Link montrera l'objet au monde en le mettant au-dessus de sa tête. C'est bon pour
|
||||
les affaires !
|
||||
|
||||
@@ -1,41 +1,28 @@
|
||||
# Guide d'installation du MultiWorld de A Link to the Past Randomizer
|
||||
|
||||
<div id="tutorial-video-container">
|
||||
<iframe width="560" height="315" src="https://www.youtube-nocookie.com/embed/mJKEHaiyR_Y" frameborder="0"
|
||||
allow="accelerometer; autoplay; encrypted-media; gyroscope; picture-in-picture" allowfullscreen>
|
||||
</iframe>
|
||||
</div>
|
||||
|
||||
## Logiciels requis
|
||||
|
||||
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
- [QUsb2Snes](https://github.com/Skarsnik/QUsb2snes/releases) (Inclus dans les utilitaires précédents)
|
||||
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases).
|
||||
- [SNI](https://github.com/alttpo/sni/releases). Inclus avec l'installation d'Archipelago ci-dessus.
|
||||
- SNI n'est pas compatible avec (Q)Usb2Snes.
|
||||
- Une solution logicielle ou matérielle capable de charger et de lancer des fichiers ROM de SNES
|
||||
- Un émulateur capable d'éxécuter des scripts Lua
|
||||
([snes9x rr](https://github.com/gocha/snes9x-rr/releases),
|
||||
[BizHawk](https://tasvideos.org/BizHawk))
|
||||
- Un SD2SNES, [FXPak Pro](https://krikzz.com/store/home/54-fxpak-pro.html), ou une autre solution matérielle
|
||||
compatible
|
||||
- Le fichier ROM de la v1.0 japonaise, sûrement nommé `Zelda no Densetsu - Kamigami no Triforce (Japan).sfc`
|
||||
- Un émulateur capable de se connecter à SNI
|
||||
[snes9x-nwa](https://github.com/Skarsnik/snes9x-emunwa/releases), ([snes9x rr](https://github.com/gocha/snes9x-rr/releases),
|
||||
[BSNES-plus](https://github.com/black-sliver/bsnes-plus),
|
||||
[BizHawk](https://tasvideos.org/BizHawk), ou
|
||||
[RetroArch](https://retroarch.com?page=platforms) 1.10.1 ou plus récent). Ou,
|
||||
- Un SD2SNES, [FXPak Pro](https://krikzz.com/store/home/54-fxpak-pro.html), ou une autre solution matérielle compatible. **À noter:
|
||||
les SNES minis ne sont pas encore supportés par SNI. Certains utilisateurs rapportent avoir du succès avec QUsb2Snes pour ce système,
|
||||
mais ce n'est pas supporté.**
|
||||
- Le fichier ROM de la v1.0 japonaise, habituellement nommé `Zelda no Densetsu - Kamigami no Triforce (Japan).sfc`
|
||||
|
||||
## Procédure d'installation
|
||||
|
||||
### Installation sur Windows
|
||||
1. Téléchargez et installez [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases). **L'installateur se situe dans la section "assets" en bas des informations de version**.
|
||||
|
||||
2. Si c'est la première fois que vous faites une génération locale ou un patch, il vous sera demandé votre fichier ROM de base. Il s'agit de votre fichier ROM Link to the Past japonais. Cet étape n'a besoin d'être faite qu'une seule fois.
|
||||
|
||||
1. Téléchargez et installez les utilitaires du MultiWorld à l'aide du lien au-dessus, faites attention à bien installer
|
||||
la version la plus récente.
|
||||
**Le fichier se situe dans la section "assets" en bas des informations de version**. Si vous voulez jouer des parties
|
||||
classiques de multiworld, téléchargez `Setup.BerserkerMultiWorld.exe`
|
||||
- Si vous voulez jouer à la version alternative avec le mélangeur de portes dans les donjons, vous téléchargez le
|
||||
fichier
|
||||
`Setup.BerserkerMultiWorld.Doors.exe`.
|
||||
- Durant le processus d'installation, il vous sera demandé de localiser votre ROM v1.0 japonaise. Si vous avez déjà
|
||||
installé le logiciel auparavant et qu'il s'agit simplement d'une mise à jour, la localisation de la ROM originale
|
||||
ne sera pas requise.
|
||||
- Il vous sera peut-être également demandé d'installer Microsoft Visual C++. Si vous le possédez déjà (possiblement
|
||||
parce qu'un jeu Steam l'a déjà installé), l'installateur ne reproposera pas de l'installer.
|
||||
|
||||
2. Si vous utilisez un émulateur, il est recommandé d'assigner votre émulateur capable d'éxécuter des scripts Lua comme
|
||||
3. Si vous utilisez un émulateur, il est recommandé d'assigner votre émulateur capable d'éxécuter des scripts Lua comme
|
||||
programme par défaut pour ouvrir vos ROMs.
|
||||
1. Extrayez votre dossier d'émulateur sur votre Bureau, ou à un endroit dont vous vous souviendrez.
|
||||
2. Faites un clic droit sur un fichier ROM et sélectionnez **Ouvrir avec...**
|
||||
@@ -44,58 +31,6 @@
|
||||
5. Naviguez dans les dossiers jusqu'au fichier `.exe` de votre émulateur et choisissez **Ouvrir**. Ce fichier
|
||||
devrait se trouver dans le dossier que vous avez extrait à la première étape.
|
||||
|
||||
### Installation sur Mac
|
||||
|
||||
- Des volontaires sont recherchés pour remplir cette section ! Contactez **Farrak Kilhn** sur Discord si vous voulez
|
||||
aider.
|
||||
|
||||
## Configurer son fichier YAML
|
||||
|
||||
### Qu'est-ce qu'un fichier YAML et pourquoi en ai-je besoin ?
|
||||
|
||||
Votre fichier YAML contient un ensemble d'options de configuration qui fournissent au générateur des informations sur
|
||||
comment il devrait générer votre seed. Chaque joueur d'un multiwolrd devra fournir son propre fichier YAML. Cela permet
|
||||
à chaque joueur d'apprécier une expérience customisée selon ses goûts, et les différents joueurs d'un même multiworld
|
||||
peuvent avoir différentes options.
|
||||
|
||||
### Où est-ce que j'obtiens un fichier YAML ?
|
||||
|
||||
La page [Génération de partie](/games/A%20Link%20to%20the%20Past/player-options) vous permet de configurer vos
|
||||
paramètres personnels et de les exporter vers un fichier YAML.
|
||||
|
||||
### Configuration avancée du fichier YAML
|
||||
|
||||
Une version plus avancée du fichier YAML peut être créée en utilisant la page
|
||||
des [paramètres de pondération](/games/A Link to the Past/weighted-options), qui vous permet de configurer jusqu'à
|
||||
trois préréglages. Cette page a de nombreuses options qui sont essentiellement représentées avec des curseurs
|
||||
glissants. Cela vous permet de choisir quelles sont les chances qu'une certaine option apparaisse par rapport aux
|
||||
autres disponibles dans une même catégorie.
|
||||
|
||||
Par exemple, imaginez que le générateur crée un seau étiqueté "Mélange des cartes", et qu'il place un morceau de papier
|
||||
pour chaque sous-option. Imaginez également que la valeur pour "On" est 20 et la valeur pour "Off" est 40.
|
||||
|
||||
Dans cet exemple, il y a soixante morceaux de papier dans le seau : vingt pour "On" et quarante pour "Off". Quand le
|
||||
générateur décide s'il doit oui ou non activer le mélange des cartes pour votre partie, , il tire aléatoirement un
|
||||
papier dans le seau. Dans cet exemple, il y a de plus grandes chances d'avoir le mélange de cartes désactivé.
|
||||
|
||||
S'il y a une option dont vous ne voulez jamais, mettez simplement sa valeur à zéro. N'oubliez pas qu'il faut que pour
|
||||
chaque paramètre il faut au moins une option qui soit paramétrée sur un nombre strictement positif.
|
||||
|
||||
### Vérifier son fichier YAML
|
||||
|
||||
Si vous voulez valider votre fichier YAML pour être sûr qu'il fonctionne, vous pouvez le vérifier sur la page du
|
||||
[Validateur de YAML](/check).
|
||||
|
||||
## Générer une partie pour un joueur
|
||||
|
||||
1. Aller sur la page [Génération de partie](/games/A%20Link%20to%20the%20Past/player-options), configurez vos options,
|
||||
et cliquez sur le bouton "Generate Game".
|
||||
2. Il vous sera alors présenté une page d'informations sur la seed, où vous pourrez télécharger votre patch.
|
||||
3. Double-cliquez sur le patch et l'émulateur devrait se lancer automatiquement avec la seed. Etant donné que le client
|
||||
n'est pas requis pour les parties à un joueur, vous pouvez le fermer ainsi que l'interface Web (WebUI).
|
||||
|
||||
## Rejoindre un MultiWorld
|
||||
|
||||
### Obtenir son patch et créer sa ROM
|
||||
|
||||
Quand vous rejoignez un multiworld, il vous sera demandé de fournir votre fichier YAML à celui qui héberge la partie ou
|
||||
@@ -109,35 +44,58 @@ automatiquement le client, et devrait créer la ROM dans le même dossier que vo
|
||||
|
||||
#### Avec un émulateur
|
||||
|
||||
Quand le client se lance automatiquement, QUsb2Snes devrait se lancer automatiquement également en arrière-plan. Si
|
||||
Quand le client se lance automatiquement, SNI devrait se lancer automatiquement également en arrière-plan. Si
|
||||
c'est la première fois qu'il démarre, il vous sera peut-être demandé de l'autoriser à communiquer à travers le pare-feu
|
||||
Windows.
|
||||
|
||||
#### snes9x-nwa
|
||||
|
||||
1. Cliquez sur 'Network Menu' et cochez **Enable Emu Network Control**
|
||||
2. Chargez votre ROM si ce n'est pas déjà fait.
|
||||
|
||||
##### snes9x-rr
|
||||
|
||||
1. Chargez votre ROM si ce n'est pas déjà fait.
|
||||
2. Cliquez sur le menu "File" et survolez l'option **Lua Scripting**
|
||||
3. Cliquez alors sur **New Lua Script Window...**
|
||||
4. Dans la nouvelle fenêtre, sélectionnez **Browse...**
|
||||
5. Dirigez vous vers le dossier où vous avez extrait snes9x-rr, allez dans le dossier `lua`, puis
|
||||
choisissez `multibridge.lua`
|
||||
6. Remarquez qu'un nom vous a été assigné, et que l'interface Web affiche "SNES Device: Connected", avec ce même nom
|
||||
dans le coin en haut à gauche.
|
||||
5. Sélectionnez le fichier lua connecteur inclus avec votre client
|
||||
- Recherchez `/SNI/lua/` dans votre fichier Archipelago.
|
||||
6. Si vous avez une erreur en chargeant le script indiquant `socket.dll missing` ou similaire, naviguez vers le fichier du
|
||||
lua que vous utilisez dans votre explorateur de fichiers et copiez le `socket.dll` à la base de votre installation snes9x.
|
||||
|
||||
#### BSNES-Plus
|
||||
|
||||
1. Chargez votre ROM si ce n'est pas déjà fait.
|
||||
2. L'émulateur devrait automatiquement se connecter lorsque SNI se lancera.
|
||||
|
||||
##### BizHawk
|
||||
|
||||
1. Assurez vous d'avoir le coeur BSNES chargé. Cela est possible en cliquant sur le menu "Tools" de BizHawk et suivant
|
||||
1. Assurez vous d'avoir le cœur BSNES chargé. Cela est possible en cliquant sur le menu "Tools" de BizHawk et suivant
|
||||
ces options de menu :
|
||||
`Config --> Cores --> SNES --> BSNES`
|
||||
Une fois le coeur changé, vous devez redémarrer BizHawk.
|
||||
- (≤ 2.8) `Config` 〉 `Cores` 〉 `SNES` 〉 `BSNES`
|
||||
- (≥ 2.9) `Config` 〉 `Preferred Cores` 〉 `SNES` 〉 `BSNESv115+`
|
||||
Une fois le cœur changé, rechargez le avec Ctrl+R (par défaut).
|
||||
2. Chargez votre ROM si ce n'est pas déjà fait.
|
||||
3. Cliquez sur le menu "Tools" et cliquez sur **Lua Console**
|
||||
4. Cliquez sur le bouton pour ouvrir un nouveau script Lua.
|
||||
5. Dirigez vous vers le dossier d'installation des utilitaires du MultiWorld, puis dans les dossiers suivants :
|
||||
`QUsb2Snes/Qusb2Snes/LuaBridge`
|
||||
6. Sélectionnez `luabridge.lua` et cliquez sur "Open".
|
||||
7. Remarquez qu'un nom vous a été assigné, et que l'interface Web affiche "SNES Device: Connected", avec ce même nom
|
||||
dans le coin en haut à gauche.
|
||||
3. Glissez et déposez le fichier `Connector.lua` que vous avez téléchargé ci-dessus sur la fenêtre principale EmuHawk.
|
||||
- Recherchez `/SNI/lua/` dans votre fichier Archipelago.
|
||||
- Vous pouvez aussi ouvrir la console Lua manuellement, cliquez sur `Script` 〉 `Open Script`, et naviguez sur `Connecteur.lua`
|
||||
avec le sélecteur de fichiers.
|
||||
|
||||
##### RetroArch 1.10.1 ou plus récent
|
||||
|
||||
Vous n'avez qu'à faire ces étapes qu'une fois.
|
||||
|
||||
1. Entrez dans le menu principal RetroArch
|
||||
2. Allez dans Réglages --> Interface utilisateur. Mettez "Afficher les réglages avancés" sur ON.
|
||||
3. Allez dans Réglages --> Réseau. Mettez "Commandes Réseau" sur ON. (trouvé sous Request Device 16.) Laissez le
|
||||
Port des commandes réseau à 555355.
|
||||
|
||||

|
||||
4. Allez dans Menu Principal --> Mise à jour en ligne --> Téléchargement de cœurs. Descendez jusqu'a"Nintendo - SNES / SFC (bsnes-mercury Performance)" et
|
||||
sélectionnez le.
|
||||
|
||||
Quand vous chargez une ROM, veillez a sélectionner un cœur **bsnes-mercury**. Ce sont les seuls cœurs qui autorisent les outils externs à lire les données d'une ROM.
|
||||
|
||||
#### Avec une solution matérielle
|
||||
|
||||
@@ -147,10 +105,7 @@ le maintenant. Les utilisateurs de SD2SNES et de FXPak Pro peuvent télécharger
|
||||
[sur cette page](http://usb2snes.com/#supported-platforms).
|
||||
|
||||
1. Fermez votre émulateur, qui s'est potentiellement lancé automatiquement.
|
||||
2. Fermez QUsb2Snes, qui s'est lancé automatiquement avec le client.
|
||||
3. Lancez la version appropriée de QUsb2Snes (v0.7.16).
|
||||
4. Lancer votre console et chargez la ROM.
|
||||
5. Remarquez que l'interface Web affiche "SNES Device: Connected", avec le nom de votre appareil.
|
||||
2. Lancez votre console et chargez la ROM.
|
||||
|
||||
### Se connecter au MultiServer
|
||||
|
||||
@@ -165,47 +120,6 @@ l'interface Web.
|
||||
|
||||
### Jouer au jeu
|
||||
|
||||
Une fois que l'interface Web affiche que la SNES et le serveur sont connectés, vous êtes prêt à jouer. Félicitations
|
||||
pour avoir rejoint un multiworld !
|
||||
|
||||
## Héberger un MultiWorld
|
||||
|
||||
La méthode recommandée pour héberger une partie est d'utiliser le service d'hébergement fourni par
|
||||
[le site](https://berserkermulti.world/generate). Le processus est relativement simple :
|
||||
|
||||
1. Récupérez les fichiers YAML des joueurs.
|
||||
2. Créez une archive zip contenant ces fichiers YAML.
|
||||
3. Téléversez l'archive zip sur le lien ci-dessus.
|
||||
4. Attendez un moment que les seed soient générées.
|
||||
5. Lorsque les seeds sont générées, vous serez redirigé vers une page d'informations "Seed Info".
|
||||
6. Cliquez sur "Create New Room". Cela vous amènera à la page du serveur. Fournissez le lien de cette page aux autres
|
||||
joueurs afin qu'ils puissent récupérer leurs patchs.
|
||||
**Note:** Les patchs fournis sur cette page permettront aux joueurs de se connecteur automatiquement au serveur,
|
||||
tandis que ceux de la page "Seed Info" non.
|
||||
7. Remarquez qu'un lien vers le traqueur du MultiWorld est en haut de la page de la salle. Vous devriez également
|
||||
fournir ce lien aux joueurs pour qu'ils puissent suivre la progression de la partie. N'importe quel personne voulant
|
||||
observer devrait avoir accès à ce lien.
|
||||
8. Une fois que tous les joueurs ont rejoint, vous pouvez commencer à jouer.
|
||||
|
||||
## Auto-tracking
|
||||
|
||||
Si vous voulez utiliser l'auto-tracking, plusieurs logiciels offrent cette possibilité.
|
||||
Le logiciel recommandé pour l'auto-tracking actuellement est
|
||||
[OpenTracker](https://github.com/trippsc2/OpenTracker/releases).
|
||||
|
||||
### Installation
|
||||
|
||||
1. Téléchargez le fichier d'installation approprié pour votre ordinateur (Les utilisateurs Windows voudront le
|
||||
fichier `.msi`).
|
||||
2. Durant le processus d'installation, il vous sera peut-être demandé d'installer les outils "Microsoft Visual Studio
|
||||
Build Tools". Un lien est fourni durant l'installation d'OpenTracker, et celle des outils doit se faire manuellement.
|
||||
|
||||
### Activer l'auto-tracking
|
||||
|
||||
1. Une fois OpenTracker démarré, cliquez sur le menu "Tracking" en haut de la fenêtre, puis choisissez **
|
||||
AutoTracker...**
|
||||
2. Appuyez sur le bouton **Get Devices**
|
||||
3. Sélectionnez votre appareil SNES dans la liste déroulante.
|
||||
4. Si vous voulez tracquer les petites clés ainsi que les objets des donjons, cochez la case **Race Illegal Tracking**
|
||||
5. Cliquez sur le bouton **Start Autotracking**
|
||||
6. Fermez la fenêtre "AutoTracker" maintenant, elle n'est plus nécessaire
|
||||
Une fois que l'interface Web affiche que la SNES et le serveur sont connectés, vous êtes prêt à jouer. Félicitations,
|
||||
vous venez de rejoindre un multiworld ! Vous pouvez exécuter différentes commandes dans votre client. Pour plus d'informations
|
||||
sur ces commandes, vous pouvez utiliser `/help` pour les commandes locales et `!help` pour les commandes serveur.
|
||||
|
||||
BIN
worlds/alttp/docs/retroarch-network-commands-fr.png
Normal file
BIN
worlds/alttp/docs/retroarch-network-commands-fr.png
Normal file
Binary file not shown.
|
After Width: | Height: | Size: 20 KiB |
@@ -684,38 +684,37 @@ class CV64PatchExtensions(APPatchExtension):
|
||||
|
||||
# Disable the 3HBs checking and setting flags when breaking them and enable their individual items checking and
|
||||
# setting flags instead.
|
||||
if options["multi_hit_breakables"]:
|
||||
rom_data.write_int32(0xE87F8, 0x00000000) # NOP
|
||||
rom_data.write_int16(0xE836C, 0x1000)
|
||||
rom_data.write_int32(0xE8B40, 0x0C0FF3CD) # JAL 0x803FCF34
|
||||
rom_data.write_int32s(0xBFCF34, patches.three_hit_item_flags_setter)
|
||||
# Villa foyer chandelier-specific functions (yeah, IDK why KCEK made different functions for this one)
|
||||
rom_data.write_int32(0xE7D54, 0x00000000) # NOP
|
||||
rom_data.write_int16(0xE7908, 0x1000)
|
||||
rom_data.write_byte(0xE7A5C, 0x10)
|
||||
rom_data.write_int32(0xE7F08, 0x0C0FF3DF) # JAL 0x803FCF7C
|
||||
rom_data.write_int32s(0xBFCF7C, patches.chandelier_item_flags_setter)
|
||||
rom_data.write_int32(0xE87F8, 0x00000000) # NOP
|
||||
rom_data.write_int16(0xE836C, 0x1000)
|
||||
rom_data.write_int32(0xE8B40, 0x0C0FF3CD) # JAL 0x803FCF34
|
||||
rom_data.write_int32s(0xBFCF34, patches.three_hit_item_flags_setter)
|
||||
# Villa foyer chandelier-specific functions (yeah, IDK why KCEK made different functions for this one)
|
||||
rom_data.write_int32(0xE7D54, 0x00000000) # NOP
|
||||
rom_data.write_int16(0xE7908, 0x1000)
|
||||
rom_data.write_byte(0xE7A5C, 0x10)
|
||||
rom_data.write_int32(0xE7F08, 0x0C0FF3DF) # JAL 0x803FCF7C
|
||||
rom_data.write_int32s(0xBFCF7C, patches.chandelier_item_flags_setter)
|
||||
|
||||
# New flag values to put in each 3HB vanilla flag's spot
|
||||
rom_data.write_int32(0x10C7C8, 0x8000FF48) # FoS dirge maiden rock
|
||||
rom_data.write_int32(0x10C7B0, 0x0200FF48) # FoS S1 bridge rock
|
||||
rom_data.write_int32(0x10C86C, 0x0010FF48) # CW upper rampart save nub
|
||||
rom_data.write_int32(0x10C878, 0x4000FF49) # CW Dracula switch slab
|
||||
rom_data.write_int32(0x10CAD8, 0x0100FF49) # Tunnel twin arrows slab
|
||||
rom_data.write_int32(0x10CAE4, 0x0004FF49) # Tunnel lonesome bucket pit rock
|
||||
rom_data.write_int32(0x10CB54, 0x4000FF4A) # UW poison parkour ledge
|
||||
rom_data.write_int32(0x10CB60, 0x0080FF4A) # UW skeleton crusher ledge
|
||||
rom_data.write_int32(0x10CBF0, 0x0008FF4A) # CC Behemoth crate
|
||||
rom_data.write_int32(0x10CC2C, 0x2000FF4B) # CC elevator pedestal
|
||||
rom_data.write_int32(0x10CC70, 0x0200FF4B) # CC lizard locker slab
|
||||
rom_data.write_int32(0x10CD88, 0x0010FF4B) # ToE pre-midsavepoint platforms ledge
|
||||
rom_data.write_int32(0x10CE6C, 0x4000FF4C) # ToSci invisible bridge crate
|
||||
rom_data.write_int32(0x10CF20, 0x0080FF4C) # CT inverted battery slab
|
||||
rom_data.write_int32(0x10CF2C, 0x0008FF4C) # CT inverted door slab
|
||||
rom_data.write_int32(0x10CF38, 0x8000FF4D) # CT final room door slab
|
||||
rom_data.write_int32(0x10CF44, 0x1000FF4D) # CT Renon slab
|
||||
rom_data.write_int32(0x10C908, 0x0008FF4D) # Villa foyer chandelier
|
||||
rom_data.write_byte(0x10CF37, 0x04) # pointer for CT final room door slab item data
|
||||
# New flag values to put in each 3HB vanilla flag's spot
|
||||
rom_data.write_int32(0x10C7C8, 0x8000FF48) # FoS dirge maiden rock
|
||||
rom_data.write_int32(0x10C7B0, 0x0200FF48) # FoS S1 bridge rock
|
||||
rom_data.write_int32(0x10C86C, 0x0010FF48) # CW upper rampart save nub
|
||||
rom_data.write_int32(0x10C878, 0x4000FF49) # CW Dracula switch slab
|
||||
rom_data.write_int32(0x10CAD8, 0x0100FF49) # Tunnel twin arrows slab
|
||||
rom_data.write_int32(0x10CAE4, 0x0004FF49) # Tunnel lonesome bucket pit rock
|
||||
rom_data.write_int32(0x10CB54, 0x4000FF4A) # UW poison parkour ledge
|
||||
rom_data.write_int32(0x10CB60, 0x0080FF4A) # UW skeleton crusher ledge
|
||||
rom_data.write_int32(0x10CBF0, 0x0008FF4A) # CC Behemoth crate
|
||||
rom_data.write_int32(0x10CC2C, 0x2000FF4B) # CC elevator pedestal
|
||||
rom_data.write_int32(0x10CC70, 0x0200FF4B) # CC lizard locker slab
|
||||
rom_data.write_int32(0x10CD88, 0x0010FF4B) # ToE pre-midsavepoint platforms ledge
|
||||
rom_data.write_int32(0x10CE6C, 0x4000FF4C) # ToSci invisible bridge crate
|
||||
rom_data.write_int32(0x10CF20, 0x0080FF4C) # CT inverted battery slab
|
||||
rom_data.write_int32(0x10CF2C, 0x0008FF4C) # CT inverted door slab
|
||||
rom_data.write_int32(0x10CF38, 0x8000FF4D) # CT final room door slab
|
||||
rom_data.write_int32(0x10CF44, 0x1000FF4D) # CT Renon slab
|
||||
rom_data.write_int32(0x10C908, 0x0008FF4D) # Villa foyer chandelier
|
||||
rom_data.write_byte(0x10CF37, 0x04) # pointer for CT final room door slab item data
|
||||
|
||||
# Once-per-frame gameplay checks
|
||||
rom_data.write_int32(0x6C848, 0x080FF40D) # J 0x803FD034
|
||||
|
||||
@@ -72,8 +72,16 @@ class DLCqworld(World):
|
||||
|
||||
self.multiworld.itempool += created_items
|
||||
|
||||
if self.options.campaign == Options.Campaign.option_basic or self.options.campaign == Options.Campaign.option_both:
|
||||
self.multiworld.early_items[self.player]["Movement Pack"] = 1
|
||||
campaign = self.options.campaign
|
||||
has_both = campaign == Options.Campaign.option_both
|
||||
has_base = campaign == Options.Campaign.option_basic or has_both
|
||||
has_big_bundles = self.options.coinsanity and self.options.coinbundlequantity > 50
|
||||
early_items = self.multiworld.early_items
|
||||
if has_base:
|
||||
if has_both and has_big_bundles:
|
||||
early_items[self.player]["Incredibly Important Pack"] = 1
|
||||
else:
|
||||
early_items[self.player]["Movement Pack"] = 1
|
||||
|
||||
for item in items_to_exclude:
|
||||
if item in self.multiworld.itempool:
|
||||
@@ -82,7 +90,7 @@ class DLCqworld(World):
|
||||
def precollect_coinsanity(self):
|
||||
if self.options.campaign == Options.Campaign.option_basic:
|
||||
if self.options.coinsanity == Options.CoinSanity.option_coin and self.options.coinbundlequantity >= 5:
|
||||
self.multiworld.push_precollected(self.create_item("Movement Pack"))
|
||||
self.multiworld.push_precollected(self.create_item("DLC Quest: Coin Bundle"))
|
||||
|
||||
def create_item(self, item: Union[str, ItemData], classification: ItemClassification = None) -> DLCQuestItem:
|
||||
if isinstance(item, str):
|
||||
|
||||
@@ -112,7 +112,7 @@ class StartWithComputerAreaMaps(Toggle):
|
||||
class ResetLevelOnDeath(DefaultOnToggle):
|
||||
"""When dying, levels are reset and monsters respawned. But inventory and checks are kept.
|
||||
Turning this setting off is considered easy mode. Good for new players that don't know the levels well."""
|
||||
display_name="Reset Level on Death"
|
||||
display_name = "Reset Level on Death"
|
||||
|
||||
|
||||
class Episode1(DefaultOnToggle):
|
||||
|
||||
@@ -102,7 +102,7 @@ class StartWithComputerAreaMaps(Toggle):
|
||||
class ResetLevelOnDeath(DefaultOnToggle):
|
||||
"""When dying, levels are reset and monsters respawned. But inventory and checks are kept.
|
||||
Turning this setting off is considered easy mode. Good for new players that don't know the levels well."""
|
||||
display_message="Reset level on death"
|
||||
display_name = "Reset Level on Death"
|
||||
|
||||
|
||||
class Episode1(DefaultOnToggle):
|
||||
|
||||
58
worlds/faxanadu/Items.py
Normal file
58
worlds/faxanadu/Items.py
Normal file
@@ -0,0 +1,58 @@
|
||||
from BaseClasses import ItemClassification
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
class ItemDef:
|
||||
def __init__(self,
|
||||
id: Optional[int],
|
||||
name: str,
|
||||
classification: ItemClassification,
|
||||
count: int,
|
||||
progression_count: int,
|
||||
prefill_location: Optional[str]):
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.classification = classification
|
||||
self.count = count
|
||||
self.progression_count = progression_count
|
||||
self.prefill_location = prefill_location
|
||||
|
||||
|
||||
items: List[ItemDef] = [
|
||||
ItemDef(400000, 'Progressive Sword', ItemClassification.progression, 4, 0, None),
|
||||
ItemDef(400001, 'Progressive Armor', ItemClassification.progression, 3, 0, None),
|
||||
ItemDef(400002, 'Progressive Shield', ItemClassification.useful, 4, 0, None),
|
||||
ItemDef(400003, 'Spring Elixir', ItemClassification.progression, 1, 0, None),
|
||||
ItemDef(400004, 'Mattock', ItemClassification.progression, 1, 0, None),
|
||||
ItemDef(400005, 'Unlock Wingboots', ItemClassification.progression, 1, 0, None),
|
||||
ItemDef(400006, 'Key Jack', ItemClassification.progression, 1, 0, None),
|
||||
ItemDef(400007, 'Key Queen', ItemClassification.progression, 1, 0, None),
|
||||
ItemDef(400008, 'Key King', ItemClassification.progression, 1, 0, None),
|
||||
ItemDef(400009, 'Key Joker', ItemClassification.progression, 1, 0, None),
|
||||
ItemDef(400010, 'Key Ace', ItemClassification.progression, 1, 0, None),
|
||||
ItemDef(400011, 'Ring of Ruby', ItemClassification.progression, 1, 0, None),
|
||||
ItemDef(400012, 'Ring of Dworf', ItemClassification.progression, 1, 0, None),
|
||||
ItemDef(400013, 'Demons Ring', ItemClassification.progression, 1, 0, None),
|
||||
ItemDef(400014, 'Black Onyx', ItemClassification.progression, 1, 0, None),
|
||||
ItemDef(None, 'Sky Spring Flow', ItemClassification.progression, 1, 0, 'Sky Spring'),
|
||||
ItemDef(None, 'Tower of Fortress Spring Flow', ItemClassification.progression, 1, 0, 'Tower of Fortress Spring'),
|
||||
ItemDef(None, 'Joker Spring Flow', ItemClassification.progression, 1, 0, 'Joker Spring'),
|
||||
ItemDef(400015, 'Deluge', ItemClassification.progression, 1, 0, None),
|
||||
ItemDef(400016, 'Thunder', ItemClassification.useful, 1, 0, None),
|
||||
ItemDef(400017, 'Fire', ItemClassification.useful, 1, 0, None),
|
||||
ItemDef(400018, 'Death', ItemClassification.useful, 1, 0, None),
|
||||
ItemDef(400019, 'Tilte', ItemClassification.useful, 1, 0, None),
|
||||
ItemDef(400020, 'Ring of Elf', ItemClassification.useful, 1, 0, None),
|
||||
ItemDef(400021, 'Magical Rod', ItemClassification.useful, 1, 0, None),
|
||||
ItemDef(400022, 'Pendant', ItemClassification.useful, 1, 0, None),
|
||||
ItemDef(400023, 'Hourglass', ItemClassification.filler, 6, 0, None),
|
||||
# We need at least 4 red potions for the Tower of Red Potion. Up to the player to save them up!
|
||||
ItemDef(400024, 'Red Potion', ItemClassification.filler, 15, 4, None),
|
||||
ItemDef(400025, 'Elixir', ItemClassification.filler, 4, 0, None),
|
||||
ItemDef(400026, 'Glove', ItemClassification.filler, 5, 0, None),
|
||||
ItemDef(400027, 'Ointment', ItemClassification.filler, 8, 0, None),
|
||||
ItemDef(400028, 'Poison', ItemClassification.trap, 13, 0, None),
|
||||
ItemDef(None, 'Killed Evil One', ItemClassification.progression, 1, 0, 'Evil One'),
|
||||
# Placeholder item so the game knows which shop slot to prefill wingboots
|
||||
ItemDef(400029, 'Wingboots', ItemClassification.useful, 0, 0, None),
|
||||
]
|
||||
199
worlds/faxanadu/Locations.py
Normal file
199
worlds/faxanadu/Locations.py
Normal file
@@ -0,0 +1,199 @@
|
||||
from typing import List, Optional
|
||||
|
||||
|
||||
class LocationType():
|
||||
world = 1 # Just standing there in the world
|
||||
hidden = 2 # Kill all monsters in the room to reveal, each "item room" counter tick.
|
||||
boss_reward = 3 # Kill a boss to reveal the item
|
||||
shop = 4 # Buy at a shop
|
||||
give = 5 # Given by an NPC
|
||||
spring = 6 # Activatable spring
|
||||
boss = 7 # Entity to kill to trigger the check
|
||||
|
||||
|
||||
class ItemType():
|
||||
unknown = 0 # Or don't care
|
||||
red_potion = 1
|
||||
|
||||
|
||||
class LocationDef:
|
||||
def __init__(self, id: Optional[int], name: str, region: str, type: int, original_item: int):
|
||||
self.id = id
|
||||
self.name = name
|
||||
self.region = region
|
||||
self.type = type
|
||||
self.original_item = original_item
|
||||
|
||||
|
||||
locations: List[LocationDef] = [
|
||||
# Eolis
|
||||
LocationDef(400100, 'Eolis Guru', 'Eolis', LocationType.give, ItemType.unknown),
|
||||
LocationDef(400101, 'Eolis Key Jack', 'Eolis', LocationType.shop, ItemType.unknown),
|
||||
LocationDef(400102, 'Eolis Hand Dagger', 'Eolis', LocationType.shop, ItemType.unknown),
|
||||
LocationDef(400103, 'Eolis Red Potion', 'Eolis', LocationType.shop, ItemType.red_potion),
|
||||
LocationDef(400104, 'Eolis Elixir', 'Eolis', LocationType.shop, ItemType.unknown),
|
||||
LocationDef(400105, 'Eolis Deluge', 'Eolis', LocationType.shop, ItemType.unknown),
|
||||
|
||||
# Path to Apolune
|
||||
LocationDef(400106, 'Path to Apolune Magic Shield', 'Path to Apolune', LocationType.shop, ItemType.unknown),
|
||||
LocationDef(400107, 'Path to Apolune Death', 'Path to Apolune', LocationType.shop, ItemType.unknown),
|
||||
|
||||
# Apolune
|
||||
LocationDef(400108, 'Apolune Small Shield', 'Apolune', LocationType.shop, ItemType.unknown),
|
||||
LocationDef(400109, 'Apolune Hand Dagger', 'Apolune', LocationType.shop, ItemType.unknown),
|
||||
LocationDef(400110, 'Apolune Deluge', 'Apolune', LocationType.shop, ItemType.unknown),
|
||||
LocationDef(400111, 'Apolune Red Potion', 'Apolune', LocationType.shop, ItemType.red_potion),
|
||||
LocationDef(400112, 'Apolune Key Jack', 'Apolune', LocationType.shop, ItemType.unknown),
|
||||
|
||||
# Tower of Trunk
|
||||
LocationDef(400113, 'Tower of Trunk Hidden Mattock', 'Tower of Trunk', LocationType.hidden, ItemType.unknown),
|
||||
LocationDef(400114, 'Tower of Trunk Hidden Hourglass', 'Tower of Trunk', LocationType.hidden, ItemType.unknown),
|
||||
LocationDef(400115, 'Tower of Trunk Boss Mattock', 'Tower of Trunk', LocationType.boss_reward, ItemType.unknown),
|
||||
|
||||
# Path to Forepaw
|
||||
LocationDef(400116, 'Path to Forepaw Hidden Red Potion', 'Path to Forepaw', LocationType.hidden, ItemType.red_potion),
|
||||
LocationDef(400117, 'Path to Forepaw Glove', 'Path to Forepaw', LocationType.world, ItemType.unknown),
|
||||
|
||||
# Forepaw
|
||||
LocationDef(400118, 'Forepaw Long Sword', 'Forepaw', LocationType.shop, ItemType.unknown),
|
||||
LocationDef(400119, 'Forepaw Studded Mail', 'Forepaw', LocationType.shop, ItemType.unknown),
|
||||
LocationDef(400120, 'Forepaw Small Shield', 'Forepaw', LocationType.shop, ItemType.unknown),
|
||||
LocationDef(400121, 'Forepaw Red Potion', 'Forepaw', LocationType.shop, ItemType.red_potion),
|
||||
LocationDef(400122, 'Forepaw Wingboots', 'Forepaw', LocationType.shop, ItemType.unknown),
|
||||
LocationDef(400123, 'Forepaw Key Jack', 'Forepaw', LocationType.shop, ItemType.unknown),
|
||||
LocationDef(400124, 'Forepaw Key Queen', 'Forepaw', LocationType.shop, ItemType.unknown),
|
||||
|
||||
# Trunk
|
||||
LocationDef(400125, 'Trunk Hidden Ointment', 'Trunk', LocationType.hidden, ItemType.unknown),
|
||||
LocationDef(400126, 'Trunk Hidden Red Potion', 'Trunk', LocationType.hidden, ItemType.red_potion),
|
||||
LocationDef(400127, 'Trunk Red Potion', 'Trunk', LocationType.world, ItemType.red_potion),
|
||||
LocationDef(None, 'Sky Spring', 'Trunk', LocationType.spring, ItemType.unknown),
|
||||
|
||||
# Joker Spring
|
||||
LocationDef(400128, 'Joker Spring Ruby Ring', 'Joker Spring', LocationType.give, ItemType.unknown),
|
||||
LocationDef(None, 'Joker Spring', 'Joker Spring', LocationType.spring, ItemType.unknown),
|
||||
|
||||
# Tower of Fortress
|
||||
LocationDef(400129, 'Tower of Fortress Poison 1', 'Tower of Fortress', LocationType.world, ItemType.unknown),
|
||||
LocationDef(400130, 'Tower of Fortress Poison 2', 'Tower of Fortress', LocationType.world, ItemType.unknown),
|
||||
LocationDef(400131, 'Tower of Fortress Hidden Wingboots', 'Tower of Fortress', LocationType.world, ItemType.unknown),
|
||||
LocationDef(400132, 'Tower of Fortress Ointment', 'Tower of Fortress', LocationType.world, ItemType.unknown),
|
||||
LocationDef(400133, 'Tower of Fortress Boss Wingboots', 'Tower of Fortress', LocationType.boss_reward, ItemType.unknown),
|
||||
LocationDef(400134, 'Tower of Fortress Elixir', 'Tower of Fortress', LocationType.world, ItemType.unknown),
|
||||
LocationDef(400135, 'Tower of Fortress Guru', 'Tower of Fortress', LocationType.give, ItemType.unknown),
|
||||
LocationDef(None, 'Tower of Fortress Spring', 'Tower of Fortress', LocationType.spring, ItemType.unknown),
|
||||
|
||||
# Path to Mascon
|
||||
LocationDef(400136, 'Path to Mascon Hidden Wingboots', 'Path to Mascon', LocationType.hidden, ItemType.unknown),
|
||||
|
||||
# Tower of Red Potion
|
||||
LocationDef(400137, 'Tower of Red Potion', 'Tower of Red Potion', LocationType.world, ItemType.red_potion),
|
||||
|
||||
# Mascon
|
||||
LocationDef(400138, 'Mascon Large Shield', 'Mascon', LocationType.shop, ItemType.unknown),
|
||||
LocationDef(400139, 'Mascon Thunder', 'Mascon', LocationType.shop, ItemType.unknown),
|
||||
LocationDef(400140, 'Mascon Mattock', 'Mascon', LocationType.shop, ItemType.unknown),
|
||||
LocationDef(400141, 'Mascon Red Potion', 'Mascon', LocationType.shop, ItemType.red_potion),
|
||||
LocationDef(400142, 'Mascon Key Jack', 'Mascon', LocationType.shop, ItemType.unknown),
|
||||
LocationDef(400143, 'Mascon Key Queen', 'Mascon', LocationType.shop, ItemType.unknown),
|
||||
|
||||
# Path to Victim
|
||||
LocationDef(400144, 'Misty Shop Death', 'Path to Victim', LocationType.shop, ItemType.unknown),
|
||||
LocationDef(400145, 'Misty Shop Hourglass', 'Path to Victim', LocationType.shop, ItemType.unknown),
|
||||
LocationDef(400146, 'Misty Shop Elixir', 'Path to Victim', LocationType.shop, ItemType.unknown),
|
||||
LocationDef(400147, 'Misty Shop Red Potion', 'Path to Victim', LocationType.shop, ItemType.red_potion),
|
||||
LocationDef(400148, 'Misty Doctor Office', 'Path to Victim', LocationType.hidden, ItemType.unknown),
|
||||
|
||||
# Tower of Suffer
|
||||
LocationDef(400149, 'Tower of Suffer Hidden Wingboots', 'Tower of Suffer', LocationType.hidden, ItemType.unknown),
|
||||
LocationDef(400150, 'Tower of Suffer Hidden Hourglass', 'Tower of Suffer', LocationType.hidden, ItemType.unknown),
|
||||
LocationDef(400151, 'Tower of Suffer Pendant', 'Tower of Suffer', LocationType.boss_reward, ItemType.unknown),
|
||||
|
||||
# Victim
|
||||
LocationDef(400152, 'Victim Full Plate', 'Victim', LocationType.shop, ItemType.unknown),
|
||||
LocationDef(400153, 'Victim Mattock', 'Victim', LocationType.shop, ItemType.unknown),
|
||||
LocationDef(400154, 'Victim Red Potion', 'Victim', LocationType.shop, ItemType.red_potion),
|
||||
LocationDef(400155, 'Victim Key King', 'Victim', LocationType.shop, ItemType.unknown),
|
||||
LocationDef(400156, 'Victim Key Queen', 'Victim', LocationType.shop, ItemType.unknown),
|
||||
LocationDef(400157, 'Victim Tavern', 'Mist', LocationType.give, ItemType.unknown),
|
||||
|
||||
# Mist
|
||||
LocationDef(400158, 'Mist Hidden Poison 1', 'Mist', LocationType.hidden, ItemType.unknown),
|
||||
LocationDef(400159, 'Mist Hidden Poison 2', 'Mist', LocationType.hidden, ItemType.unknown),
|
||||
LocationDef(400160, 'Mist Hidden Wingboots', 'Mist', LocationType.hidden, ItemType.unknown),
|
||||
LocationDef(400161, 'Misty Magic Hall', 'Mist', LocationType.give, ItemType.unknown),
|
||||
LocationDef(400162, 'Misty House', 'Mist', LocationType.give, ItemType.unknown),
|
||||
|
||||
# Useless Tower
|
||||
LocationDef(400163, 'Useless Tower', 'Useless Tower', LocationType.hidden, ItemType.unknown),
|
||||
|
||||
# Tower of Mist
|
||||
LocationDef(400164, 'Tower of Mist Hidden Ointment', 'Tower of Mist', LocationType.hidden, ItemType.unknown),
|
||||
LocationDef(400165, 'Tower of Mist Elixir', 'Tower of Mist', LocationType.world, ItemType.unknown),
|
||||
LocationDef(400166, 'Tower of Mist Black Onyx', 'Tower of Mist', LocationType.boss_reward, ItemType.unknown),
|
||||
|
||||
# Path to Conflate
|
||||
LocationDef(400167, 'Path to Conflate Hidden Ointment', 'Path to Conflate', LocationType.hidden, ItemType.unknown),
|
||||
LocationDef(400168, 'Path to Conflate Poison', 'Path to Conflate', LocationType.hidden, ItemType.unknown),
|
||||
|
||||
# Helm Branch
|
||||
LocationDef(400169, 'Helm Branch Hidden Glove', 'Helm Branch', LocationType.hidden, ItemType.unknown),
|
||||
LocationDef(400170, 'Helm Branch Battle Helmet', 'Helm Branch', LocationType.boss_reward, ItemType.unknown),
|
||||
|
||||
# Conflate
|
||||
LocationDef(400171, 'Conflate Giant Blade', 'Conflate', LocationType.shop, ItemType.unknown),
|
||||
LocationDef(400172, 'Conflate Magic Shield', 'Conflate', LocationType.shop, ItemType.unknown),
|
||||
LocationDef(400173, 'Conflate Wingboots', 'Conflate', LocationType.shop, ItemType.unknown),
|
||||
LocationDef(400174, 'Conflate Red Potion', 'Conflate', LocationType.shop, ItemType.red_potion),
|
||||
LocationDef(400175, 'Conflate Guru', 'Conflate', LocationType.give, ItemType.unknown),
|
||||
|
||||
# Branches
|
||||
LocationDef(400176, 'Branches Hidden Ointment', 'Branches', LocationType.hidden, ItemType.unknown),
|
||||
LocationDef(400177, 'Branches Poison', 'Branches', LocationType.world, ItemType.unknown),
|
||||
LocationDef(400178, 'Branches Hidden Mattock', 'Branches', LocationType.hidden, ItemType.unknown),
|
||||
LocationDef(400179, 'Branches Hidden Hourglass', 'Branches', LocationType.hidden, ItemType.unknown),
|
||||
|
||||
# Path to Daybreak
|
||||
LocationDef(400180, 'Path to Daybreak Hidden Wingboots 1', 'Path to Daybreak', LocationType.hidden, ItemType.unknown),
|
||||
LocationDef(400181, 'Path to Daybreak Magical Rod', 'Path to Daybreak', LocationType.world, ItemType.unknown),
|
||||
LocationDef(400182, 'Path to Daybreak Hidden Wingboots 2', 'Path to Daybreak', LocationType.hidden, ItemType.unknown),
|
||||
LocationDef(400183, 'Path to Daybreak Poison', 'Path to Daybreak', LocationType.world, ItemType.unknown),
|
||||
LocationDef(400184, 'Path to Daybreak Glove', 'Path to Daybreak', LocationType.world, ItemType.unknown),
|
||||
LocationDef(400185, 'Path to Daybreak Battle Suit', 'Path to Daybreak', LocationType.boss_reward, ItemType.unknown),
|
||||
|
||||
# Daybreak
|
||||
LocationDef(400186, 'Daybreak Tilte', 'Daybreak', LocationType.shop, ItemType.unknown),
|
||||
LocationDef(400187, 'Daybreak Giant Blade', 'Daybreak', LocationType.shop, ItemType.unknown),
|
||||
LocationDef(400188, 'Daybreak Red Potion', 'Daybreak', LocationType.shop, ItemType.red_potion),
|
||||
LocationDef(400189, 'Daybreak Key King', 'Daybreak', LocationType.shop, ItemType.unknown),
|
||||
LocationDef(400190, 'Daybreak Key Queen', 'Daybreak', LocationType.shop, ItemType.unknown),
|
||||
|
||||
# Dartmoor Castle
|
||||
LocationDef(400191, 'Dartmoor Castle Hidden Hourglass', 'Dartmoor Castle', LocationType.hidden, ItemType.unknown),
|
||||
LocationDef(400192, 'Dartmoor Castle Hidden Red Potion', 'Dartmoor Castle', LocationType.hidden, ItemType.red_potion),
|
||||
|
||||
# Dartmoor
|
||||
LocationDef(400193, 'Dartmoor Giant Blade', 'Dartmoor', LocationType.shop, ItemType.unknown),
|
||||
LocationDef(400194, 'Dartmoor Red Potion', 'Dartmoor', LocationType.shop, ItemType.red_potion),
|
||||
LocationDef(400195, 'Dartmoor Key King', 'Dartmoor', LocationType.shop, ItemType.unknown),
|
||||
|
||||
# Fraternal Castle
|
||||
LocationDef(400196, 'Fraternal Castle Hidden Ointment', 'Fraternal Castle', LocationType.hidden, ItemType.unknown),
|
||||
LocationDef(400197, 'Fraternal Castle Shop Hidden Ointment', 'Fraternal Castle', LocationType.hidden, ItemType.unknown),
|
||||
LocationDef(400198, 'Fraternal Castle Poison 1', 'Fraternal Castle', LocationType.world, ItemType.unknown),
|
||||
LocationDef(400199, 'Fraternal Castle Poison 2', 'Fraternal Castle', LocationType.world, ItemType.unknown),
|
||||
LocationDef(400200, 'Fraternal Castle Poison 3', 'Fraternal Castle', LocationType.world, ItemType.unknown),
|
||||
# LocationDef(400201, 'Fraternal Castle Red Potion', 'Fraternal Castle', LocationType.world, ItemType.red_potion), # This location is inaccessible. Keeping commented for context.
|
||||
LocationDef(400202, 'Fraternal Castle Hidden Hourglass', 'Fraternal Castle', LocationType.hidden, ItemType.unknown),
|
||||
LocationDef(400203, 'Fraternal Castle Dragon Slayer', 'Fraternal Castle', LocationType.boss_reward, ItemType.unknown),
|
||||
LocationDef(400204, 'Fraternal Castle Guru', 'Fraternal Castle', LocationType.give, ItemType.unknown),
|
||||
|
||||
# Evil Fortress
|
||||
LocationDef(400205, 'Evil Fortress Ointment', 'Evil Fortress', LocationType.world, ItemType.unknown),
|
||||
LocationDef(400206, 'Evil Fortress Poison 1', 'Evil Fortress', LocationType.world, ItemType.unknown),
|
||||
LocationDef(400207, 'Evil Fortress Glove', 'Evil Fortress', LocationType.world, ItemType.unknown),
|
||||
LocationDef(400208, 'Evil Fortress Poison 2', 'Evil Fortress', LocationType.world, ItemType.unknown),
|
||||
LocationDef(400209, 'Evil Fortress Poison 3', 'Evil Fortress', LocationType.world, ItemType.unknown),
|
||||
LocationDef(400210, 'Evil Fortress Hidden Glove', 'Evil Fortress', LocationType.hidden, ItemType.unknown),
|
||||
LocationDef(None, 'Evil One', 'Evil Fortress', LocationType.boss, ItemType.unknown),
|
||||
]
|
||||
107
worlds/faxanadu/Options.py
Normal file
107
worlds/faxanadu/Options.py
Normal file
@@ -0,0 +1,107 @@
|
||||
from Options import PerGameCommonOptions, Toggle, DefaultOnToggle, StartInventoryPool, Choice
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
class KeepShopRedPotions(Toggle):
|
||||
"""
|
||||
Prevents the Shop's Red Potions from being shuffled. Those locations
|
||||
will have purchasable Red Potion as usual for their usual price.
|
||||
"""
|
||||
display_name = "Keep Shop Red Potions"
|
||||
|
||||
|
||||
class IncludePendant(Toggle):
|
||||
"""
|
||||
Pendant is an item that boosts your attack power permanently when picked up.
|
||||
However, due to a programming error in the original game, it has the reverse
|
||||
effect. You start with the Pendant power, and lose it when picking
|
||||
it up. So this item is essentially a trap.
|
||||
There is a setting in the client to reverse the effect back to its original intend.
|
||||
This could be used in conjunction with this option to increase or lower difficulty.
|
||||
"""
|
||||
display_name = "Include Pendant"
|
||||
|
||||
|
||||
class IncludePoisons(DefaultOnToggle):
|
||||
"""
|
||||
Whether or not to include Poison Potions in the pool of items. Including them
|
||||
effectively turn them into traps in multiplayer.
|
||||
"""
|
||||
display_name = "Include Poisons"
|
||||
|
||||
|
||||
class RequireDragonSlayer(Toggle):
|
||||
"""
|
||||
Requires the Dragon Slayer to be available before fighting the final boss is required.
|
||||
Turning this on will turn Progressive Shields into progression items.
|
||||
|
||||
This setting does not force you to use Dragon Slayer to kill the final boss.
|
||||
Instead, it ensures that you will have the Dragon Slayer and be able to equip
|
||||
it before you are expected to beat the final boss.
|
||||
"""
|
||||
display_name = "Require Dragon Slayer"
|
||||
|
||||
|
||||
class RandomMusic(Toggle):
|
||||
"""
|
||||
All levels' music is shuffled. Except the title screen because it's finite.
|
||||
This is an aesthetic option and doesn't affect gameplay.
|
||||
"""
|
||||
display_name = "Random Musics"
|
||||
|
||||
|
||||
class RandomSound(Toggle):
|
||||
"""
|
||||
All sounds are shuffled.
|
||||
This is an aesthetic option and doesn't affect gameplay.
|
||||
"""
|
||||
display_name = "Random Sounds"
|
||||
|
||||
|
||||
class RandomNPC(Toggle):
|
||||
"""
|
||||
NPCs and their portraits are shuffled.
|
||||
This is an aesthetic option and doesn't affect gameplay.
|
||||
"""
|
||||
display_name = "Random NPCs"
|
||||
|
||||
|
||||
class RandomMonsters(Choice):
|
||||
"""
|
||||
Choose how monsters are randomized.
|
||||
"Vanilla": No randomization
|
||||
"Level Shuffle": Monsters are shuffled within a level
|
||||
"Level Random": Monsters are picked randomly, balanced based on the ratio of the current level
|
||||
"World Shuffle": Monsters are shuffled across the entire world
|
||||
"World Random": Monsters are picked randomly, balanced based on the ratio of the entire world
|
||||
"Chaotic": Completely random, except big vs small ratio is kept. Big are mini-bosses.
|
||||
"""
|
||||
display_name = "Random Monsters"
|
||||
option_vanilla = 0
|
||||
option_level_shuffle = 1
|
||||
option_level_random = 2
|
||||
option_world_shuffle = 3
|
||||
option_world_random = 4
|
||||
option_chaotic = 5
|
||||
default = 0
|
||||
|
||||
|
||||
class RandomRewards(Toggle):
|
||||
"""
|
||||
Monsters drops are shuffled.
|
||||
"""
|
||||
display_name = "Random Rewards"
|
||||
|
||||
|
||||
@dataclass
|
||||
class FaxanaduOptions(PerGameCommonOptions):
|
||||
start_inventory_from_pool: StartInventoryPool
|
||||
keep_shop_red_potions: KeepShopRedPotions
|
||||
include_pendant: IncludePendant
|
||||
include_poisons: IncludePoisons
|
||||
require_dragon_slayer: RequireDragonSlayer
|
||||
random_musics: RandomMusic
|
||||
random_sounds: RandomSound
|
||||
random_npcs: RandomNPC
|
||||
random_monsters: RandomMonsters
|
||||
random_rewards: RandomRewards
|
||||
66
worlds/faxanadu/Regions.py
Normal file
66
worlds/faxanadu/Regions.py
Normal file
@@ -0,0 +1,66 @@
|
||||
from BaseClasses import Region
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import FaxanaduWorld
|
||||
|
||||
|
||||
def create_region(name, player, multiworld):
|
||||
region = Region(name, player, multiworld)
|
||||
multiworld.regions.append(region)
|
||||
return region
|
||||
|
||||
|
||||
def create_regions(faxanadu_world: "FaxanaduWorld"):
|
||||
player = faxanadu_world.player
|
||||
multiworld = faxanadu_world.multiworld
|
||||
|
||||
# Create regions
|
||||
menu = create_region("Menu", player, multiworld)
|
||||
eolis = create_region("Eolis", player, multiworld)
|
||||
path_to_apolune = create_region("Path to Apolune", player, multiworld)
|
||||
apolune = create_region("Apolune", player, multiworld)
|
||||
create_region("Tower of Trunk", player, multiworld)
|
||||
path_to_forepaw = create_region("Path to Forepaw", player, multiworld)
|
||||
forepaw = create_region("Forepaw", player, multiworld)
|
||||
trunk = create_region("Trunk", player, multiworld)
|
||||
create_region("Joker Spring", player, multiworld)
|
||||
create_region("Tower of Fortress", player, multiworld)
|
||||
path_to_mascon = create_region("Path to Mascon", player, multiworld)
|
||||
create_region("Tower of Red Potion", player, multiworld)
|
||||
mascon = create_region("Mascon", player, multiworld)
|
||||
path_to_victim = create_region("Path to Victim", player, multiworld)
|
||||
create_region("Tower of Suffer", player, multiworld)
|
||||
victim = create_region("Victim", player, multiworld)
|
||||
mist = create_region("Mist", player, multiworld)
|
||||
create_region("Useless Tower", player, multiworld)
|
||||
create_region("Tower of Mist", player, multiworld)
|
||||
path_to_conflate = create_region("Path to Conflate", player, multiworld)
|
||||
create_region("Helm Branch", player, multiworld)
|
||||
create_region("Conflate", player, multiworld)
|
||||
branches = create_region("Branches", player, multiworld)
|
||||
path_to_daybreak = create_region("Path to Daybreak", player, multiworld)
|
||||
daybreak = create_region("Daybreak", player, multiworld)
|
||||
dartmoor_castle = create_region("Dartmoor Castle", player, multiworld)
|
||||
create_region("Dartmoor", player, multiworld)
|
||||
create_region("Fraternal Castle", player, multiworld)
|
||||
create_region("Evil Fortress", player, multiworld)
|
||||
|
||||
# Create connections
|
||||
menu.add_exits(["Eolis"])
|
||||
eolis.add_exits(["Path to Apolune"])
|
||||
path_to_apolune.add_exits(["Apolune"])
|
||||
apolune.add_exits(["Tower of Trunk", "Path to Forepaw"])
|
||||
path_to_forepaw.add_exits(["Forepaw"])
|
||||
forepaw.add_exits(["Trunk"])
|
||||
trunk.add_exits(["Joker Spring", "Tower of Fortress", "Path to Mascon"])
|
||||
path_to_mascon.add_exits(["Tower of Red Potion", "Mascon"])
|
||||
mascon.add_exits(["Path to Victim"])
|
||||
path_to_victim.add_exits(["Tower of Suffer", "Victim"])
|
||||
victim.add_exits(["Mist"])
|
||||
mist.add_exits(["Useless Tower", "Tower of Mist", "Path to Conflate"])
|
||||
path_to_conflate.add_exits(["Helm Branch", "Conflate", "Branches"])
|
||||
branches.add_exits(["Path to Daybreak"])
|
||||
path_to_daybreak.add_exits(["Daybreak"])
|
||||
daybreak.add_exits(["Dartmoor Castle"])
|
||||
dartmoor_castle.add_exits(["Dartmoor", "Fraternal Castle", "Evil Fortress"])
|
||||
79
worlds/faxanadu/Rules.py
Normal file
79
worlds/faxanadu/Rules.py
Normal file
@@ -0,0 +1,79 @@
|
||||
from typing import TYPE_CHECKING
|
||||
from worlds.generic.Rules import set_rule
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import FaxanaduWorld
|
||||
|
||||
|
||||
def can_buy_in_eolis(state, player):
|
||||
# Sword or Deluge so we can farm for gold.
|
||||
# Ring of Elf so we can get 1500 from the King.
|
||||
return state.has_any(["Progressive Sword", "Deluge", "Ring of Elf"], player)
|
||||
|
||||
|
||||
def has_any_magic(state, player):
|
||||
return state.has_any(["Deluge", "Thunder", "Fire", "Death", "Tilte"], player)
|
||||
|
||||
|
||||
def set_rules(faxanadu_world: "FaxanaduWorld"):
|
||||
player = faxanadu_world.player
|
||||
multiworld = faxanadu_world.multiworld
|
||||
|
||||
# Region rules
|
||||
set_rule(multiworld.get_entrance("Eolis -> Path to Apolune", player), lambda state:
|
||||
state.has_all(["Key Jack", "Progressive Sword"], player)) # You can't go far with magic only
|
||||
set_rule(multiworld.get_entrance("Apolune -> Tower of Trunk", player), lambda state: state.has("Key Jack", player))
|
||||
set_rule(multiworld.get_entrance("Apolune -> Path to Forepaw", player), lambda state: state.has("Mattock", player))
|
||||
set_rule(multiworld.get_entrance("Trunk -> Joker Spring", player), lambda state: state.has("Key Joker", player))
|
||||
set_rule(multiworld.get_entrance("Trunk -> Tower of Fortress", player), lambda state: state.has("Key Jack", player))
|
||||
set_rule(multiworld.get_entrance("Trunk -> Path to Mascon", player), lambda state:
|
||||
state.has_all(["Key Queen", "Ring of Ruby", "Sky Spring Flow", "Tower of Fortress Spring Flow", "Joker Spring Flow"], player) and
|
||||
state.has("Progressive Sword", player, 2))
|
||||
set_rule(multiworld.get_entrance("Path to Mascon -> Tower of Red Potion", player), lambda state:
|
||||
state.has("Key Queen", player) and
|
||||
state.has("Red Potion", player, 4)) # It's impossible to go through the tower of Red Potion without at least 1-2 potions. Give them 4 for good measure.
|
||||
set_rule(multiworld.get_entrance("Path to Victim -> Tower of Suffer", player), lambda state: state.has("Key Queen", player))
|
||||
set_rule(multiworld.get_entrance("Path to Victim -> Victim", player), lambda state: state.has("Unlock Wingboots", player))
|
||||
set_rule(multiworld.get_entrance("Mist -> Useless Tower", player), lambda state:
|
||||
state.has_all(["Key King", "Unlock Wingboots"], player))
|
||||
set_rule(multiworld.get_entrance("Mist -> Tower of Mist", player), lambda state: state.has("Key King", player))
|
||||
set_rule(multiworld.get_entrance("Mist -> Path to Conflate", player), lambda state: state.has("Key Ace", player))
|
||||
set_rule(multiworld.get_entrance("Path to Conflate -> Helm Branch", player), lambda state: state.has("Key King", player))
|
||||
set_rule(multiworld.get_entrance("Path to Conflate -> Branches", player), lambda state: state.has("Key King", player))
|
||||
set_rule(multiworld.get_entrance("Daybreak -> Dartmoor Castle", player), lambda state: state.has("Ring of Dworf", player))
|
||||
set_rule(multiworld.get_entrance("Dartmoor Castle -> Evil Fortress", player), lambda state: state.has("Demons Ring", player))
|
||||
|
||||
# Location rules
|
||||
set_rule(multiworld.get_location("Eolis Key Jack", player), lambda state: can_buy_in_eolis(state, player))
|
||||
set_rule(multiworld.get_location("Eolis Hand Dagger", player), lambda state: can_buy_in_eolis(state, player))
|
||||
set_rule(multiworld.get_location("Eolis Elixir", player), lambda state: can_buy_in_eolis(state, player))
|
||||
set_rule(multiworld.get_location("Eolis Deluge", player), lambda state: can_buy_in_eolis(state, player))
|
||||
set_rule(multiworld.get_location("Eolis Red Potion", player), lambda state: can_buy_in_eolis(state, player))
|
||||
set_rule(multiworld.get_location("Path to Apolune Magic Shield", player), lambda state: state.has("Key King", player)) # Mid-late cost, make sure we've progressed
|
||||
set_rule(multiworld.get_location("Path to Apolune Death", player), lambda state: state.has("Key Ace", player)) # Mid-late cost, make sure we've progressed
|
||||
set_rule(multiworld.get_location("Tower of Trunk Hidden Mattock", player), lambda state:
|
||||
# This is actually possible if the monster drop into the stairs and kill it with dagger. But it's a "pro move"
|
||||
state.has("Deluge", player, 1) or
|
||||
state.has("Progressive Sword", player, 2))
|
||||
set_rule(multiworld.get_location("Path to Forepaw Glove", player), lambda state:
|
||||
state.has_all(["Deluge", "Unlock Wingboots"], player))
|
||||
set_rule(multiworld.get_location("Trunk Red Potion", player), lambda state: state.has("Unlock Wingboots", player))
|
||||
set_rule(multiworld.get_location("Sky Spring", player), lambda state: state.has("Unlock Wingboots", player))
|
||||
set_rule(multiworld.get_location("Tower of Fortress Spring", player), lambda state: state.has("Spring Elixir", player))
|
||||
set_rule(multiworld.get_location("Tower of Fortress Guru", player), lambda state: state.has("Sky Spring Flow", player))
|
||||
set_rule(multiworld.get_location("Tower of Suffer Hidden Wingboots", player), lambda state:
|
||||
state.has("Deluge", player) or
|
||||
state.has("Progressive Sword", player, 2))
|
||||
set_rule(multiworld.get_location("Misty House", player), lambda state: state.has("Black Onyx", player))
|
||||
set_rule(multiworld.get_location("Misty Doctor Office", player), lambda state: has_any_magic(state, player))
|
||||
set_rule(multiworld.get_location("Conflate Guru", player), lambda state: state.has("Progressive Armor", player, 3))
|
||||
set_rule(multiworld.get_location("Branches Hidden Mattock", player), lambda state: state.has("Unlock Wingboots", player))
|
||||
set_rule(multiworld.get_location("Path to Daybreak Glove", player), lambda state: state.has("Unlock Wingboots", player))
|
||||
set_rule(multiworld.get_location("Dartmoor Castle Hidden Hourglass", player), lambda state: state.has("Unlock Wingboots", player))
|
||||
set_rule(multiworld.get_location("Dartmoor Castle Hidden Red Potion", player), lambda state: has_any_magic(state, player))
|
||||
set_rule(multiworld.get_location("Fraternal Castle Guru", player), lambda state: state.has("Progressive Sword", player, 4))
|
||||
set_rule(multiworld.get_location("Fraternal Castle Shop Hidden Ointment", player), lambda state: has_any_magic(state, player))
|
||||
|
||||
if faxanadu_world.options.require_dragon_slayer.value:
|
||||
set_rule(multiworld.get_location("Evil One", player), lambda state:
|
||||
state.has_all_counts({"Progressive Sword": 4, "Progressive Armor": 3, "Progressive Shield": 4}, player))
|
||||
190
worlds/faxanadu/__init__.py
Normal file
190
worlds/faxanadu/__init__.py
Normal file
@@ -0,0 +1,190 @@
|
||||
from typing import Any, Dict, List
|
||||
|
||||
from BaseClasses import Item, Location, Tutorial, ItemClassification, MultiWorld
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
from . import Items, Locations, Regions, Rules
|
||||
from .Options import FaxanaduOptions
|
||||
from worlds.generic.Rules import set_rule
|
||||
|
||||
|
||||
DAXANADU_VERSION = "0.3.0"
|
||||
|
||||
|
||||
class FaxanaduLocation(Location):
|
||||
game: str = "Faxanadu"
|
||||
|
||||
|
||||
class FaxanaduItem(Item):
|
||||
game: str = "Faxanadu"
|
||||
|
||||
|
||||
class FaxanaduWeb(WebWorld):
|
||||
tutorials = [Tutorial(
|
||||
"Multiworld Setup Guide",
|
||||
"A guide to setting up the Faxanadu randomizer connected to an Archipelago Multiworld",
|
||||
"English",
|
||||
"setup_en.md",
|
||||
"setup/en",
|
||||
["Daivuk"]
|
||||
)]
|
||||
theme = "dirt"
|
||||
|
||||
|
||||
class FaxanaduWorld(World):
|
||||
"""
|
||||
Faxanadu is an action role-playing platform video game developed by Hudson Soft for the Nintendo Entertainment System
|
||||
"""
|
||||
options_dataclass = FaxanaduOptions
|
||||
options: FaxanaduOptions
|
||||
game = "Faxanadu"
|
||||
web = FaxanaduWeb()
|
||||
|
||||
item_name_to_id = {item.name: item.id for item in Items.items if item.id is not None}
|
||||
item_name_to_item = {item.name: item for item in Items.items}
|
||||
location_name_to_id = {loc.name: loc.id for loc in Locations.locations if loc.id is not None}
|
||||
|
||||
def __init__(self, world: MultiWorld, player: int):
|
||||
self.filler_ratios: Dict[str, int] = {}
|
||||
|
||||
super().__init__(world, player)
|
||||
|
||||
def create_regions(self):
|
||||
Regions.create_regions(self)
|
||||
|
||||
# Add locations into regions
|
||||
for region in self.multiworld.get_regions(self.player):
|
||||
for loc in [location for location in Locations.locations if location.region == region.name]:
|
||||
location = FaxanaduLocation(self.player, loc.name, loc.id, region)
|
||||
|
||||
# In Faxanadu, Poison hurts you when picked up. It makes no sense to sell them in shops
|
||||
if loc.type == Locations.LocationType.shop:
|
||||
location.item_rule = lambda item, player=self.player: not (player == item.player and item.name == "Poison")
|
||||
|
||||
region.locations.append(location)
|
||||
|
||||
def set_rules(self):
|
||||
Rules.set_rules(self)
|
||||
self.multiworld.completion_condition[self.player] = lambda state: state.has("Killed Evil One", self.player)
|
||||
|
||||
def create_item(self, name: str) -> FaxanaduItem:
|
||||
item: Items.ItemDef = self.item_name_to_item[name]
|
||||
return FaxanaduItem(name, item.classification, item.id, self.player)
|
||||
|
||||
# Returns how many red potions were prefilled into shops
|
||||
def prefill_shop_red_potions(self) -> int:
|
||||
red_potion_in_shop_count = 0
|
||||
if self.options.keep_shop_red_potions:
|
||||
red_potion_item = self.item_name_to_item["Red Potion"]
|
||||
red_potion_shop_locations = [
|
||||
loc
|
||||
for loc in Locations.locations
|
||||
if loc.type == Locations.LocationType.shop and loc.original_item == Locations.ItemType.red_potion
|
||||
]
|
||||
for loc in red_potion_shop_locations:
|
||||
location = self.get_location(loc.name)
|
||||
location.place_locked_item(FaxanaduItem(red_potion_item.name, red_potion_item.classification, red_potion_item.id, self.player))
|
||||
red_potion_in_shop_count += 1
|
||||
return red_potion_in_shop_count
|
||||
|
||||
def put_wingboot_in_shop(self, shops, region_name):
|
||||
item = self.item_name_to_item["Wingboots"]
|
||||
shop = shops.pop(region_name)
|
||||
slot = self.random.randint(0, len(shop) - 1)
|
||||
loc = shop[slot]
|
||||
location = self.get_location(loc.name)
|
||||
location.place_locked_item(FaxanaduItem(item.name, item.classification, item.id, self.player))
|
||||
|
||||
# Put a rule right away that we need to have to unlocked.
|
||||
set_rule(location, lambda state: state.has("Unlock Wingboots", self.player))
|
||||
|
||||
# Returns how many wingboots were prefilled into shops
|
||||
def prefill_shop_wingboots(self) -> int:
|
||||
# Collect shops
|
||||
shops: Dict[str, List[Locations.LocationDef]] = {}
|
||||
for loc in Locations.locations:
|
||||
if loc.type == Locations.LocationType.shop:
|
||||
if self.options.keep_shop_red_potions and loc.original_item == Locations.ItemType.red_potion:
|
||||
continue # Don't override our red potions
|
||||
shops.setdefault(loc.region, []).append(loc)
|
||||
|
||||
shop_count = len(shops)
|
||||
wingboots_count = round(shop_count / 2.5) # On 10 shops, we should have about 4 shops with wingboots
|
||||
|
||||
# At least one should be in the first 4 shops. Because we require wingboots to progress past that point.
|
||||
must_have_regions = [region for i, region in enumerate(shops) if i < 4]
|
||||
self.put_wingboot_in_shop(shops, self.random.choice(must_have_regions))
|
||||
|
||||
# Fill in the rest randomly in remaining shops
|
||||
for i in range(wingboots_count - 1): # -1 because we added one already
|
||||
region = self.random.choice(list(shops.keys()))
|
||||
self.put_wingboot_in_shop(shops, region)
|
||||
|
||||
return wingboots_count
|
||||
|
||||
def create_items(self) -> None:
|
||||
itempool: List[FaxanaduItem] = []
|
||||
|
||||
# Prefill red potions in shops if option is set
|
||||
red_potion_in_shop_count = self.prefill_shop_red_potions()
|
||||
|
||||
# Prefill wingboots in shops
|
||||
wingboots_in_shop_count = self.prefill_shop_wingboots()
|
||||
|
||||
# Create the item pool, excluding fillers.
|
||||
prefilled_count = red_potion_in_shop_count + wingboots_in_shop_count
|
||||
for item in Items.items:
|
||||
# Ignore pendant if turned off
|
||||
if item.name == "Pendant" and not self.options.include_pendant:
|
||||
continue
|
||||
|
||||
# ignore fillers for now, we will fill them later
|
||||
if item.classification in [ItemClassification.filler, ItemClassification.trap] and \
|
||||
item.progression_count == 0:
|
||||
continue
|
||||
|
||||
prefill_loc = None
|
||||
if item.prefill_location:
|
||||
prefill_loc = self.get_location(item.prefill_location)
|
||||
|
||||
# if require dragon slayer is turned on, we need progressive shields to be progression
|
||||
item_classification = item.classification
|
||||
if self.options.require_dragon_slayer and item.name == "Progressive Shield":
|
||||
item_classification = ItemClassification.progression
|
||||
|
||||
if prefill_loc:
|
||||
prefill_loc.place_locked_item(FaxanaduItem(item.name, item_classification, item.id, self.player))
|
||||
prefilled_count += 1
|
||||
else:
|
||||
for i in range(item.count - item.progression_count):
|
||||
itempool.append(FaxanaduItem(item.name, item_classification, item.id, self.player))
|
||||
for i in range(item.progression_count):
|
||||
itempool.append(FaxanaduItem(item.name, ItemClassification.progression, item.id, self.player))
|
||||
|
||||
# Set up filler ratios
|
||||
self.filler_ratios = {
|
||||
item.name: item.count
|
||||
for item in Items.items
|
||||
if item.classification in [ItemClassification.filler, ItemClassification.trap]
|
||||
}
|
||||
|
||||
# If red potions are locked in shops, remove the count from the ratio.
|
||||
self.filler_ratios["Red Potion"] -= red_potion_in_shop_count
|
||||
|
||||
# Remove poisons if not desired
|
||||
if not self.options.include_poisons:
|
||||
self.filler_ratios["Poison"] = 0
|
||||
|
||||
# Randomly add fillers to the pool with ratios based on og game occurrence counts.
|
||||
filler_count = len(Locations.locations) - len(itempool) - prefilled_count
|
||||
for i in range(filler_count):
|
||||
itempool.append(self.create_item(self.get_filler_item_name()))
|
||||
|
||||
self.multiworld.itempool += itempool
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
return self.random.choices(list(self.filler_ratios.keys()), weights=list(self.filler_ratios.values()))[0]
|
||||
|
||||
def fill_slot_data(self) -> Dict[str, Any]:
|
||||
slot_data = self.options.as_dict("keep_shop_red_potions", "random_musics", "random_sounds", "random_npcs", "random_monsters", "random_rewards")
|
||||
slot_data["daxanadu_version"] = DAXANADU_VERSION
|
||||
return slot_data
|
||||
27
worlds/faxanadu/docs/en_Faxanadu.md
Normal file
27
worlds/faxanadu/docs/en_Faxanadu.md
Normal file
@@ -0,0 +1,27 @@
|
||||
# Faxanadu
|
||||
|
||||
## Where is the settings page?
|
||||
|
||||
The [player options page](../player-options) contains the options needed to configure your game session.
|
||||
|
||||
## What does randomization do to this game?
|
||||
|
||||
All game items collected in the map, shops, and boss drops are randomized.
|
||||
|
||||
Keys are unique. Once you get the Jack Key, you can open all Jack doors; the key stays in your inventory.
|
||||
|
||||
Wingboots are randomized across shops only. They are LOCKED and cannot be bought until you get the item that unlocks them.
|
||||
|
||||
Normal Elixirs don't revive the tower spring. A new item, Spring Elixir, is necessary. This new item is unique.
|
||||
|
||||
## What is the goal?
|
||||
|
||||
The goal is to kill the Evil One.
|
||||
|
||||
## What is a "check" in The Faxanadu?
|
||||
|
||||
Shop items, item locations in the world, boss drops, and secret items.
|
||||
|
||||
## What "items" can you unlock in Faxanadu?
|
||||
|
||||
Keys, Armors, Weapons, Potions, Shields, Magics, Poisons, Gloves, etc.
|
||||
32
worlds/faxanadu/docs/setup_en.md
Normal file
32
worlds/faxanadu/docs/setup_en.md
Normal file
@@ -0,0 +1,32 @@
|
||||
# Faxanadu Randomizer Setup
|
||||
|
||||
## Required Software
|
||||
|
||||
- [Daxanadu](https://github.com/Daivuk/Daxanadu/releases/)
|
||||
- Faxanadu ROM, English version
|
||||
|
||||
## Optional Software
|
||||
|
||||
- [ArchipelagoTextClient](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
|
||||
## Installing Daxanadu
|
||||
1. Download [Daxanadu.zip](https://github.com/Daivuk/Daxanadu/releases/) and extract it.
|
||||
2. Copy your rom `Faxanadu (U).nes` into the newly extracted folder.
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
1. Launch Daxanadu.exe
|
||||
2. From the Main menu, go to the `ARCHIPELAGO` menu. Enter the server's address, slot name, and password. Then select `PLAY`.
|
||||
3. Enjoy!
|
||||
|
||||
To continue a game, follow the same connection steps.
|
||||
Connecting with a different seed won't erase your progress in other seeds.
|
||||
|
||||
## Archipelago Text Client
|
||||
|
||||
We recommend having Archipelago's Text Client open on the side to keep track of what items you receive and send.
|
||||
Daxanadu doesn't display messages. You'll only get popups when picking them up.
|
||||
|
||||
## Auto-Tracking
|
||||
|
||||
Daxanadu has an integrated tracker that can be toggled in the options.
|
||||
@@ -2,8 +2,8 @@
|
||||
Archipelago does not have a compiled release on macOS. However, it is possible to run from source code on macOS. This guide expects you to have some experience with running software from the terminal.
|
||||
## Prerequisite Software
|
||||
Here is a list of software to install and source code to download.
|
||||
1. Python 3.9 "universal2" or newer from the [macOS Python downloads page](https://www.python.org/downloads/macos/).
|
||||
**Python 3.11 is not supported yet.**
|
||||
1. Python 3.10 "universal2" or newer from the [macOS Python downloads page](https://www.python.org/downloads/macos/).
|
||||
**Python 3.13 is not supported yet.**
|
||||
2. Xcode from the [macOS App Store](https://apps.apple.com/us/app/xcode/id497799835).
|
||||
3. The source code from the [Archipelago releases page](https://github.com/ArchipelagoMW/Archipelago/releases).
|
||||
4. The asset with darwin in the name from the [SNI Github releases page](https://github.com/alttpo/sni/releases).
|
||||
|
||||
@@ -104,7 +104,7 @@ class StartWithMapScrolls(Toggle):
|
||||
class ResetLevelOnDeath(DefaultOnToggle):
|
||||
"""When dying, levels are reset and monsters respawned. But inventory and checks are kept.
|
||||
Turning this setting off is considered easy mode. Good for new players that don't know the levels well."""
|
||||
display_message="Reset level on death"
|
||||
display_name = "Reset Level on Death"
|
||||
|
||||
|
||||
class CheckSanity(Toggle):
|
||||
|
||||
@@ -300,7 +300,7 @@ class PlandoCharmCosts(OptionDict):
|
||||
display_name = "Charm Notch Cost Plando"
|
||||
valid_keys = frozenset(charm_names)
|
||||
schema = Schema({
|
||||
Optional(name): And(int, lambda n: 6 >= n >= 0) for name in charm_names
|
||||
Optional(name): And(int, lambda n: 6 >= n >= 0, error="Charm costs must be integers in the range 0-6.") for name in charm_names
|
||||
})
|
||||
|
||||
def get_costs(self, charm_costs: typing.List[int]) -> typing.List[int]:
|
||||
|
||||
@@ -183,7 +183,7 @@ class MuseDashWorld(World):
|
||||
if album:
|
||||
return MuseDashSongItem(name, self.player, album)
|
||||
|
||||
song = self.md_collection.song_items.get(name)
|
||||
song = self.md_collection.song_items[name]
|
||||
return MuseDashSongItem(name, self.player, song)
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
|
||||
@@ -1,7 +1,7 @@
|
||||
import typing
|
||||
import random
|
||||
from dataclasses import dataclass
|
||||
from Options import Option, DefaultOnToggle, Toggle, Range, OptionList, OptionSet, DeathLink, PlandoConnections, \
|
||||
from Options import Option, DefaultOnToggle, Toggle, Range, OptionSet, DeathLink, PlandoConnections, \
|
||||
PerGameCommonOptions, OptionGroup
|
||||
from .EntranceShuffle import entrance_shuffle_table
|
||||
from .LogicTricks import normalized_name_tricks
|
||||
@@ -1272,7 +1272,7 @@ sfx_options: typing.Dict[str, type(Option)] = {
|
||||
}
|
||||
|
||||
|
||||
class LogicTricks(OptionList):
|
||||
class LogicTricks(OptionSet):
|
||||
"""Set various tricks for logic in Ocarina of Time.
|
||||
Format as a comma-separated list of "nice" names: ["Fewer Tunic Requirements", "Hidden Grottos without Stone of Agony"].
|
||||
A full list of supported tricks can be found at:
|
||||
|
||||
@@ -2,6 +2,7 @@
|
||||
|
||||
### Features
|
||||
|
||||
- Added many new item and location groups.
|
||||
- Added a Swedish translation of the setup guide.
|
||||
- The client communicates map transitions to any trackers connected to the slot.
|
||||
- Added the player's Normalize Encounter Rates option to slot data for trackers.
|
||||
|
||||
@@ -15,11 +15,11 @@ import settings
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
|
||||
from .client import PokemonEmeraldClient # Unused, but required to register with BizHawkClient
|
||||
from .data import LEGENDARY_POKEMON, MapData, SpeciesData, TrainerData, data as emerald_data
|
||||
from .items import (ITEM_GROUPS, PokemonEmeraldItem, create_item_label_to_code_map, get_item_classification,
|
||||
offset_item_value)
|
||||
from .locations import (LOCATION_GROUPS, PokemonEmeraldLocation, create_location_label_to_id_map,
|
||||
create_locations_with_tags, set_free_fly, set_legendary_cave_entrances)
|
||||
from .data import LEGENDARY_POKEMON, MapData, SpeciesData, TrainerData, LocationCategory, data as emerald_data
|
||||
from .groups import ITEM_GROUPS, LOCATION_GROUPS
|
||||
from .items import PokemonEmeraldItem, create_item_label_to_code_map, get_item_classification, offset_item_value
|
||||
from .locations import (PokemonEmeraldLocation, create_location_label_to_id_map, create_locations_by_category,
|
||||
set_free_fly, set_legendary_cave_entrances)
|
||||
from .opponents import randomize_opponent_parties
|
||||
from .options import (Goal, DarkCavesRequireFlash, HmRequirements, ItemPoolType, PokemonEmeraldOptions,
|
||||
RandomizeWildPokemon, RandomizeBadges, RandomizeHms, NormanRequirement)
|
||||
@@ -133,9 +133,10 @@ class PokemonEmeraldWorld(World):
|
||||
|
||||
@classmethod
|
||||
def stage_assert_generate(cls, multiworld: MultiWorld) -> None:
|
||||
from .sanity_check import validate_regions
|
||||
from .sanity_check import validate_regions, validate_group_maps
|
||||
|
||||
assert validate_regions()
|
||||
assert validate_group_maps()
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
return "Great Ball"
|
||||
@@ -237,24 +238,32 @@ class PokemonEmeraldWorld(World):
|
||||
|
||||
def create_regions(self) -> None:
|
||||
from .regions import create_regions
|
||||
regions = create_regions(self)
|
||||
all_regions = create_regions(self)
|
||||
|
||||
tags = {"Badge", "HM", "KeyItem", "Rod", "Bike", "EventTicket"} # Tags with progression items always included
|
||||
# Categories with progression items always included
|
||||
categories = {
|
||||
LocationCategory.BADGE,
|
||||
LocationCategory.HM,
|
||||
LocationCategory.KEY,
|
||||
LocationCategory.ROD,
|
||||
LocationCategory.BIKE,
|
||||
LocationCategory.TICKET
|
||||
}
|
||||
if self.options.overworld_items:
|
||||
tags.add("OverworldItem")
|
||||
categories.add(LocationCategory.OVERWORLD_ITEM)
|
||||
if self.options.hidden_items:
|
||||
tags.add("HiddenItem")
|
||||
categories.add(LocationCategory.HIDDEN_ITEM)
|
||||
if self.options.npc_gifts:
|
||||
tags.add("NpcGift")
|
||||
categories.add(LocationCategory.GIFT)
|
||||
if self.options.berry_trees:
|
||||
tags.add("BerryTree")
|
||||
categories.add(LocationCategory.BERRY_TREE)
|
||||
if self.options.dexsanity:
|
||||
tags.add("Pokedex")
|
||||
categories.add(LocationCategory.POKEDEX)
|
||||
if self.options.trainersanity:
|
||||
tags.add("Trainer")
|
||||
create_locations_with_tags(self, regions, tags)
|
||||
categories.add(LocationCategory.TRAINER)
|
||||
create_locations_by_category(self, all_regions, categories)
|
||||
|
||||
self.multiworld.regions.extend(regions.values())
|
||||
self.multiworld.regions.extend(all_regions.values())
|
||||
|
||||
# Exclude locations which are always locked behind the player's goal
|
||||
def exclude_locations(location_names: List[str]):
|
||||
@@ -325,21 +334,21 @@ class PokemonEmeraldWorld(World):
|
||||
# Filter progression items which shouldn't be shuffled into the itempool.
|
||||
# Their locations will still exist, but event items will be placed and
|
||||
# locked at their vanilla locations instead.
|
||||
filter_tags = set()
|
||||
filter_categories = set()
|
||||
|
||||
if not self.options.key_items:
|
||||
filter_tags.add("KeyItem")
|
||||
filter_categories.add(LocationCategory.KEY)
|
||||
if not self.options.rods:
|
||||
filter_tags.add("Rod")
|
||||
filter_categories.add(LocationCategory.ROD)
|
||||
if not self.options.bikes:
|
||||
filter_tags.add("Bike")
|
||||
filter_categories.add(LocationCategory.BIKE)
|
||||
if not self.options.event_tickets:
|
||||
filter_tags.add("EventTicket")
|
||||
filter_categories.add(LocationCategory.TICKET)
|
||||
|
||||
if self.options.badges in {RandomizeBadges.option_vanilla, RandomizeBadges.option_shuffle}:
|
||||
filter_tags.add("Badge")
|
||||
filter_categories.add(LocationCategory.BADGE)
|
||||
if self.options.hms in {RandomizeHms.option_vanilla, RandomizeHms.option_shuffle}:
|
||||
filter_tags.add("HM")
|
||||
filter_categories.add(LocationCategory.HM)
|
||||
|
||||
# If Badges and HMs are set to the `shuffle` option, don't add them to
|
||||
# the normal item pool, but do create their items and save them and
|
||||
@@ -347,17 +356,17 @@ class PokemonEmeraldWorld(World):
|
||||
if self.options.badges == RandomizeBadges.option_shuffle:
|
||||
self.badge_shuffle_info = [
|
||||
(location, self.create_item_by_code(location.default_item_code))
|
||||
for location in [l for l in item_locations if "Badge" in l.tags]
|
||||
for location in [l for l in item_locations if emerald_data.locations[l.key].category == LocationCategory.BADGE]
|
||||
]
|
||||
if self.options.hms == RandomizeHms.option_shuffle:
|
||||
self.hm_shuffle_info = [
|
||||
(location, self.create_item_by_code(location.default_item_code))
|
||||
for location in [l for l in item_locations if "HM" in l.tags]
|
||||
for location in [l for l in item_locations if emerald_data.locations[l.key].category == LocationCategory.HM]
|
||||
]
|
||||
|
||||
# Filter down locations to actual items that will be filled and create
|
||||
# the itempool.
|
||||
item_locations = [location for location in item_locations if len(filter_tags & location.tags) == 0]
|
||||
item_locations = [location for location in item_locations if emerald_data.locations[location.key].category not in filter_categories]
|
||||
default_itempool = [self.create_item_by_code(location.default_item_code) for location in item_locations]
|
||||
|
||||
# Take the itempool as-is
|
||||
@@ -366,7 +375,8 @@ class PokemonEmeraldWorld(World):
|
||||
|
||||
# Recreate the itempool from random items
|
||||
elif self.options.item_pool_type in (ItemPoolType.option_diverse, ItemPoolType.option_diverse_balanced):
|
||||
item_categories = ["Ball", "Heal", "Candy", "Vitamin", "EvoStone", "Money", "TM", "Held", "Misc", "Berry"]
|
||||
item_categories = ["Ball", "Healing", "Rare Candy", "Vitamin", "Evolution Stone",
|
||||
"Money", "TM", "Held", "Misc", "Berry"]
|
||||
|
||||
# Count occurrences of types of vanilla items in pool
|
||||
item_category_counter = Counter()
|
||||
@@ -436,25 +446,26 @@ class PokemonEmeraldWorld(World):
|
||||
|
||||
# Key items which are considered in access rules but not randomized are converted to events and placed
|
||||
# in their vanilla locations so that the player can have them in their inventory for logic.
|
||||
def convert_unrandomized_items_to_events(tag: str) -> None:
|
||||
def convert_unrandomized_items_to_events(category: LocationCategory) -> None:
|
||||
for location in self.multiworld.get_locations(self.player):
|
||||
if location.tags is not None and tag in location.tags:
|
||||
assert isinstance(location, PokemonEmeraldLocation)
|
||||
if location.key is not None and emerald_data.locations[location.key].category == category:
|
||||
location.place_locked_item(self.create_event(self.item_id_to_name[location.default_item_code]))
|
||||
location.progress_type = LocationProgressType.DEFAULT
|
||||
location.address = None
|
||||
|
||||
if self.options.badges == RandomizeBadges.option_vanilla:
|
||||
convert_unrandomized_items_to_events("Badge")
|
||||
convert_unrandomized_items_to_events(LocationCategory.BADGE)
|
||||
if self.options.hms == RandomizeHms.option_vanilla:
|
||||
convert_unrandomized_items_to_events("HM")
|
||||
convert_unrandomized_items_to_events(LocationCategory.HM)
|
||||
if not self.options.rods:
|
||||
convert_unrandomized_items_to_events("Rod")
|
||||
convert_unrandomized_items_to_events(LocationCategory.ROD)
|
||||
if not self.options.bikes:
|
||||
convert_unrandomized_items_to_events("Bike")
|
||||
convert_unrandomized_items_to_events(LocationCategory.BIKE)
|
||||
if not self.options.event_tickets:
|
||||
convert_unrandomized_items_to_events("EventTicket")
|
||||
convert_unrandomized_items_to_events(LocationCategory.TICKET)
|
||||
if not self.options.key_items:
|
||||
convert_unrandomized_items_to_events("KeyItem")
|
||||
convert_unrandomized_items_to_events(LocationCategory.KEY)
|
||||
|
||||
def pre_fill(self) -> None:
|
||||
# Badges and HMs that are set to shuffle need to be placed at
|
||||
|
||||
@@ -117,6 +117,21 @@ class ItemData(NamedTuple):
|
||||
tags: FrozenSet[str]
|
||||
|
||||
|
||||
class LocationCategory(IntEnum):
|
||||
BADGE = 0
|
||||
HM = 1
|
||||
KEY = 2
|
||||
ROD = 3
|
||||
BIKE = 4
|
||||
TICKET = 5
|
||||
OVERWORLD_ITEM = 6
|
||||
HIDDEN_ITEM = 7
|
||||
GIFT = 8
|
||||
BERRY_TREE = 9
|
||||
TRAINER = 10
|
||||
POKEDEX = 11
|
||||
|
||||
|
||||
class LocationData(NamedTuple):
|
||||
name: str
|
||||
label: str
|
||||
@@ -124,6 +139,7 @@ class LocationData(NamedTuple):
|
||||
default_item: int
|
||||
address: Union[int, List[int]]
|
||||
flag: int
|
||||
category: LocationCategory
|
||||
tags: FrozenSet[str]
|
||||
|
||||
|
||||
@@ -431,6 +447,7 @@ def _init() -> None:
|
||||
location_json["default_item"],
|
||||
[location_json["address"]] + [j["address"] for j in alternate_rival_jsons],
|
||||
location_json["flag"],
|
||||
LocationCategory[location_attributes_json[location_name]["category"]],
|
||||
frozenset(location_attributes_json[location_name]["tags"])
|
||||
)
|
||||
else:
|
||||
@@ -441,6 +458,7 @@ def _init() -> None:
|
||||
location_json["default_item"],
|
||||
location_json["address"],
|
||||
location_json["flag"],
|
||||
LocationCategory[location_attributes_json[location_name]["category"]],
|
||||
frozenset(location_attributes_json[location_name]["tags"])
|
||||
)
|
||||
new_region.locations.append(location_name)
|
||||
@@ -948,6 +966,7 @@ def _init() -> None:
|
||||
evo_stage_to_ball_map[evo_stage],
|
||||
data.locations[dex_location_name].address,
|
||||
data.locations[dex_location_name].flag,
|
||||
data.locations[dex_location_name].category,
|
||||
data.locations[dex_location_name].tags
|
||||
)
|
||||
|
||||
|
||||
@@ -52,49 +52,49 @@
|
||||
"ITEM_HM_CUT": {
|
||||
"label": "HM01 Cut",
|
||||
"classification": "PROGRESSION",
|
||||
"tags": ["HM", "Unique"],
|
||||
"tags": ["HM", "HM01", "Unique"],
|
||||
"modern_id": 420
|
||||
},
|
||||
"ITEM_HM_FLY": {
|
||||
"label": "HM02 Fly",
|
||||
"classification": "PROGRESSION",
|
||||
"tags": ["HM", "Unique"],
|
||||
"tags": ["HM", "HM02", "Unique"],
|
||||
"modern_id": 421
|
||||
},
|
||||
"ITEM_HM_SURF": {
|
||||
"label": "HM03 Surf",
|
||||
"classification": "PROGRESSION",
|
||||
"tags": ["HM", "Unique"],
|
||||
"tags": ["HM", "HM03", "Unique"],
|
||||
"modern_id": 422
|
||||
},
|
||||
"ITEM_HM_STRENGTH": {
|
||||
"label": "HM04 Strength",
|
||||
"classification": "PROGRESSION",
|
||||
"tags": ["HM", "Unique"],
|
||||
"tags": ["HM", "HM04", "Unique"],
|
||||
"modern_id": 423
|
||||
},
|
||||
"ITEM_HM_FLASH": {
|
||||
"label": "HM05 Flash",
|
||||
"classification": "PROGRESSION",
|
||||
"tags": ["HM", "Unique"],
|
||||
"tags": ["HM", "HM05", "Unique"],
|
||||
"modern_id": 424
|
||||
},
|
||||
"ITEM_HM_ROCK_SMASH": {
|
||||
"label": "HM06 Rock Smash",
|
||||
"classification": "PROGRESSION",
|
||||
"tags": ["HM", "Unique"],
|
||||
"tags": ["HM", "HM06", "Unique"],
|
||||
"modern_id": 425
|
||||
},
|
||||
"ITEM_HM_WATERFALL": {
|
||||
"label": "HM07 Waterfall",
|
||||
"classification": "PROGRESSION",
|
||||
"tags": ["HM", "Unique"],
|
||||
"tags": ["HM", "HM07", "Unique"],
|
||||
"modern_id": 737
|
||||
},
|
||||
"ITEM_HM_DIVE": {
|
||||
"label": "HM08 Dive",
|
||||
"classification": "PROGRESSION",
|
||||
"tags": ["HM", "Unique"],
|
||||
"tags": ["HM", "HM08", "Unique"],
|
||||
"modern_id": null
|
||||
},
|
||||
|
||||
@@ -375,169 +375,169 @@
|
||||
"ITEM_POTION": {
|
||||
"label": "Potion",
|
||||
"classification": "FILLER",
|
||||
"tags": ["Heal"],
|
||||
"tags": ["Healing"],
|
||||
"modern_id": 17
|
||||
},
|
||||
"ITEM_ANTIDOTE": {
|
||||
"label": "Antidote",
|
||||
"classification": "FILLER",
|
||||
"tags": ["Heal"],
|
||||
"tags": ["Healing"],
|
||||
"modern_id": 18
|
||||
},
|
||||
"ITEM_BURN_HEAL": {
|
||||
"label": "Burn Heal",
|
||||
"classification": "FILLER",
|
||||
"tags": ["Heal"],
|
||||
"tags": ["Healing"],
|
||||
"modern_id": 19
|
||||
},
|
||||
"ITEM_ICE_HEAL": {
|
||||
"label": "Ice Heal",
|
||||
"classification": "FILLER",
|
||||
"tags": ["Heal"],
|
||||
"tags": ["Healing"],
|
||||
"modern_id": 20
|
||||
},
|
||||
"ITEM_AWAKENING": {
|
||||
"label": "Awakening",
|
||||
"classification": "FILLER",
|
||||
"tags": ["Heal"],
|
||||
"tags": ["Healing"],
|
||||
"modern_id": 21
|
||||
},
|
||||
"ITEM_PARALYZE_HEAL": {
|
||||
"label": "Paralyze Heal",
|
||||
"classification": "FILLER",
|
||||
"tags": ["Heal"],
|
||||
"tags": ["Healing"],
|
||||
"modern_id": 22
|
||||
},
|
||||
"ITEM_FULL_RESTORE": {
|
||||
"label": "Full Restore",
|
||||
"classification": "FILLER",
|
||||
"tags": ["Heal"],
|
||||
"tags": ["Healing"],
|
||||
"modern_id": 23
|
||||
},
|
||||
"ITEM_MAX_POTION": {
|
||||
"label": "Max Potion",
|
||||
"classification": "FILLER",
|
||||
"tags": ["Heal"],
|
||||
"tags": ["Healing"],
|
||||
"modern_id": 24
|
||||
},
|
||||
"ITEM_HYPER_POTION": {
|
||||
"label": "Hyper Potion",
|
||||
"classification": "FILLER",
|
||||
"tags": ["Heal"],
|
||||
"tags": ["Healing"],
|
||||
"modern_id": 25
|
||||
},
|
||||
"ITEM_SUPER_POTION": {
|
||||
"label": "Super Potion",
|
||||
"classification": "FILLER",
|
||||
"tags": ["Heal"],
|
||||
"tags": ["Healing"],
|
||||
"modern_id": 26
|
||||
},
|
||||
"ITEM_FULL_HEAL": {
|
||||
"label": "Full Heal",
|
||||
"classification": "FILLER",
|
||||
"tags": ["Heal"],
|
||||
"tags": ["Healing"],
|
||||
"modern_id": 27
|
||||
},
|
||||
"ITEM_REVIVE": {
|
||||
"label": "Revive",
|
||||
"classification": "FILLER",
|
||||
"tags": ["Heal"],
|
||||
"tags": ["Healing"],
|
||||
"modern_id": 28
|
||||
},
|
||||
"ITEM_MAX_REVIVE": {
|
||||
"label": "Max Revive",
|
||||
"classification": "FILLER",
|
||||
"tags": ["Heal"],
|
||||
"tags": ["Healing"],
|
||||
"modern_id": 29
|
||||
},
|
||||
"ITEM_FRESH_WATER": {
|
||||
"label": "Fresh Water",
|
||||
"classification": "FILLER",
|
||||
"tags": ["Heal"],
|
||||
"tags": ["Healing"],
|
||||
"modern_id": 30
|
||||
},
|
||||
"ITEM_SODA_POP": {
|
||||
"label": "Soda Pop",
|
||||
"classification": "FILLER",
|
||||
"tags": ["Heal"],
|
||||
"tags": ["Healing"],
|
||||
"modern_id": 31
|
||||
},
|
||||
"ITEM_LEMONADE": {
|
||||
"label": "Lemonade",
|
||||
"classification": "FILLER",
|
||||
"tags": ["Heal"],
|
||||
"tags": ["Healing"],
|
||||
"modern_id": 32
|
||||
},
|
||||
"ITEM_MOOMOO_MILK": {
|
||||
"label": "Moomoo Milk",
|
||||
"classification": "FILLER",
|
||||
"tags": ["Heal"],
|
||||
"tags": ["Healing"],
|
||||
"modern_id": 33
|
||||
},
|
||||
"ITEM_ENERGY_POWDER": {
|
||||
"label": "Energy Powder",
|
||||
"classification": "FILLER",
|
||||
"tags": ["Heal"],
|
||||
"tags": ["Healing"],
|
||||
"modern_id": 34
|
||||
},
|
||||
"ITEM_ENERGY_ROOT": {
|
||||
"label": "Energy Root",
|
||||
"classification": "FILLER",
|
||||
"tags": ["Heal"],
|
||||
"tags": ["Healing"],
|
||||
"modern_id": 35
|
||||
},
|
||||
"ITEM_HEAL_POWDER": {
|
||||
"label": "Heal Powder",
|
||||
"classification": "FILLER",
|
||||
"tags": ["Heal"],
|
||||
"tags": ["Healing"],
|
||||
"modern_id": 36
|
||||
},
|
||||
"ITEM_REVIVAL_HERB": {
|
||||
"label": "Revival Herb",
|
||||
"classification": "FILLER",
|
||||
"tags": ["Heal"],
|
||||
"tags": ["Healing"],
|
||||
"modern_id": 37
|
||||
},
|
||||
"ITEM_ETHER": {
|
||||
"label": "Ether",
|
||||
"classification": "FILLER",
|
||||
"tags": ["Heal"],
|
||||
"tags": ["Healing"],
|
||||
"modern_id": 38
|
||||
},
|
||||
"ITEM_MAX_ETHER": {
|
||||
"label": "Max Ether",
|
||||
"classification": "FILLER",
|
||||
"tags": ["Heal"],
|
||||
"tags": ["Healing"],
|
||||
"modern_id": 39
|
||||
},
|
||||
"ITEM_ELIXIR": {
|
||||
"label": "Elixir",
|
||||
"classification": "FILLER",
|
||||
"tags": ["Heal"],
|
||||
"tags": ["Healing"],
|
||||
"modern_id": 40
|
||||
},
|
||||
"ITEM_MAX_ELIXIR": {
|
||||
"label": "Max Elixir",
|
||||
"classification": "FILLER",
|
||||
"tags": ["Heal"],
|
||||
"tags": ["Healing"],
|
||||
"modern_id": 41
|
||||
},
|
||||
"ITEM_LAVA_COOKIE": {
|
||||
"label": "Lava Cookie",
|
||||
"classification": "FILLER",
|
||||
"tags": ["Heal"],
|
||||
"tags": ["Healing"],
|
||||
"modern_id": 42
|
||||
},
|
||||
"ITEM_BERRY_JUICE": {
|
||||
"label": "Berry Juice",
|
||||
"classification": "FILLER",
|
||||
"tags": ["Heal"],
|
||||
"tags": ["Healing"],
|
||||
"modern_id": 43
|
||||
},
|
||||
"ITEM_SACRED_ASH": {
|
||||
"label": "Sacred Ash",
|
||||
"classification": "USEFUL",
|
||||
"tags": ["Heal"],
|
||||
"tags": ["Healing"],
|
||||
"modern_id": 44
|
||||
},
|
||||
|
||||
@@ -736,19 +736,19 @@
|
||||
},
|
||||
"ITEM_BLACK_FLUTE": {
|
||||
"label": "Black Flute",
|
||||
"classification": "FILLER",
|
||||
"classification": "USEFUL",
|
||||
"tags": ["Misc"],
|
||||
"modern_id": 68
|
||||
},
|
||||
"ITEM_WHITE_FLUTE": {
|
||||
"label": "White Flute",
|
||||
"classification": "FILLER",
|
||||
"classification": "USEFUL",
|
||||
"tags": ["Misc"],
|
||||
"modern_id": 69
|
||||
},
|
||||
"ITEM_HEART_SCALE": {
|
||||
"label": "Heart Scale",
|
||||
"classification": "FILLER",
|
||||
"classification": "USEFUL",
|
||||
"tags": ["Misc"],
|
||||
"modern_id": 93
|
||||
},
|
||||
@@ -757,37 +757,37 @@
|
||||
"ITEM_SUN_STONE": {
|
||||
"label": "Sun Stone",
|
||||
"classification": "USEFUL",
|
||||
"tags": ["EvoStone"],
|
||||
"tags": ["Evolution Stone"],
|
||||
"modern_id": 80
|
||||
},
|
||||
"ITEM_MOON_STONE": {
|
||||
"label": "Moon Stone",
|
||||
"classification": "USEFUL",
|
||||
"tags": ["EvoStone"],
|
||||
"tags": ["Evolution Stone"],
|
||||
"modern_id": 81
|
||||
},
|
||||
"ITEM_FIRE_STONE": {
|
||||
"label": "Fire Stone",
|
||||
"classification": "USEFUL",
|
||||
"tags": ["EvoStone"],
|
||||
"tags": ["Evolution Stone"],
|
||||
"modern_id": 82
|
||||
},
|
||||
"ITEM_THUNDER_STONE": {
|
||||
"label": "Thunder Stone",
|
||||
"classification": "USEFUL",
|
||||
"tags": ["EvoStone"],
|
||||
"tags": ["Evolution Stone"],
|
||||
"modern_id": 83
|
||||
},
|
||||
"ITEM_WATER_STONE": {
|
||||
"label": "Water Stone",
|
||||
"classification": "USEFUL",
|
||||
"tags": ["EvoStone"],
|
||||
"tags": ["Evolution Stone"],
|
||||
"modern_id": 84
|
||||
},
|
||||
"ITEM_LEAF_STONE": {
|
||||
"label": "Leaf Stone",
|
||||
"classification": "USEFUL",
|
||||
"tags": ["EvoStone"],
|
||||
"tags": ["Evolution Stone"],
|
||||
"modern_id": 85
|
||||
},
|
||||
|
||||
@@ -1215,7 +1215,7 @@
|
||||
"ITEM_KINGS_ROCK": {
|
||||
"label": "King's Rock",
|
||||
"classification": "USEFUL",
|
||||
"tags": ["Held"],
|
||||
"tags": ["Held", "Evolution Stone"],
|
||||
"modern_id": 221
|
||||
},
|
||||
"ITEM_SILVER_POWDER": {
|
||||
@@ -1245,13 +1245,13 @@
|
||||
"ITEM_DEEP_SEA_TOOTH": {
|
||||
"label": "Deep Sea Tooth",
|
||||
"classification": "USEFUL",
|
||||
"tags": ["Held"],
|
||||
"tags": ["Held", "Evolution Stone"],
|
||||
"modern_id": 226
|
||||
},
|
||||
"ITEM_DEEP_SEA_SCALE": {
|
||||
"label": "Deep Sea Scale",
|
||||
"classification": "USEFUL",
|
||||
"tags": ["Held"],
|
||||
"tags": ["Held", "Evolution Stone"],
|
||||
"modern_id": 227
|
||||
},
|
||||
"ITEM_SMOKE_BALL": {
|
||||
@@ -1287,7 +1287,7 @@
|
||||
"ITEM_METAL_COAT": {
|
||||
"label": "Metal Coat",
|
||||
"classification": "USEFUL",
|
||||
"tags": ["Held"],
|
||||
"tags": ["Held", "Evolution Stone"],
|
||||
"modern_id": 233
|
||||
},
|
||||
"ITEM_LEFTOVERS": {
|
||||
@@ -1299,7 +1299,7 @@
|
||||
"ITEM_DRAGON_SCALE": {
|
||||
"label": "Dragon Scale",
|
||||
"classification": "USEFUL",
|
||||
"tags": ["Held"],
|
||||
"tags": ["Held", "Evolution Stone"],
|
||||
"modern_id": 235
|
||||
},
|
||||
"ITEM_LIGHT_BALL": {
|
||||
@@ -1401,7 +1401,7 @@
|
||||
"ITEM_UP_GRADE": {
|
||||
"label": "Up-Grade",
|
||||
"classification": "USEFUL",
|
||||
"tags": ["Held"],
|
||||
"tags": ["Held", "Evolution Stone"],
|
||||
"modern_id": 252
|
||||
},
|
||||
"ITEM_SHELL_BELL": {
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
721
worlds/pokemon_emerald/groups.py
Normal file
721
worlds/pokemon_emerald/groups.py
Normal file
@@ -0,0 +1,721 @@
|
||||
from typing import Dict, Set
|
||||
|
||||
from .data import LocationCategory, data
|
||||
|
||||
|
||||
# Item Groups
|
||||
ITEM_GROUPS: Dict[str, Set[str]] = {}
|
||||
|
||||
for item in data.items.values():
|
||||
for tag in item.tags:
|
||||
if tag not in ITEM_GROUPS:
|
||||
ITEM_GROUPS[tag] = set()
|
||||
ITEM_GROUPS[tag].add(item.label)
|
||||
|
||||
# Location Groups
|
||||
_LOCATION_GROUP_MAPS = {
|
||||
"Abandoned Ship": {
|
||||
"MAP_ABANDONED_SHIP_CAPTAINS_OFFICE",
|
||||
"MAP_ABANDONED_SHIP_CORRIDORS_1F",
|
||||
"MAP_ABANDONED_SHIP_CORRIDORS_B1F",
|
||||
"MAP_ABANDONED_SHIP_DECK",
|
||||
"MAP_ABANDONED_SHIP_HIDDEN_FLOOR_CORRIDORS",
|
||||
"MAP_ABANDONED_SHIP_HIDDEN_FLOOR_ROOMS",
|
||||
"MAP_ABANDONED_SHIP_ROOMS2_1F",
|
||||
"MAP_ABANDONED_SHIP_ROOMS2_B1F",
|
||||
"MAP_ABANDONED_SHIP_ROOMS_1F",
|
||||
"MAP_ABANDONED_SHIP_ROOMS_B1F",
|
||||
"MAP_ABANDONED_SHIP_ROOM_B1F",
|
||||
"MAP_ABANDONED_SHIP_UNDERWATER1",
|
||||
"MAP_ABANDONED_SHIP_UNDERWATER2",
|
||||
},
|
||||
"Aqua Hideout": {
|
||||
"MAP_AQUA_HIDEOUT_1F",
|
||||
"MAP_AQUA_HIDEOUT_B1F",
|
||||
"MAP_AQUA_HIDEOUT_B2F",
|
||||
},
|
||||
"Battle Frontier": {
|
||||
"MAP_ARTISAN_CAVE_1F",
|
||||
"MAP_ARTISAN_CAVE_B1F",
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_ARENA_BATTLE_ROOM",
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_ARENA_CORRIDOR",
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_ARENA_LOBBY",
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_DOME_BATTLE_ROOM",
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_DOME_CORRIDOR",
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_DOME_LOBBY",
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_DOME_PRE_BATTLE_ROOM",
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_FACTORY_BATTLE_ROOM",
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_FACTORY_LOBBY",
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_FACTORY_PRE_BATTLE_ROOM",
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_PALACE_BATTLE_ROOM",
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_PALACE_CORRIDOR",
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_PALACE_LOBBY",
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_PIKE_CORRIDOR",
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_PIKE_LOBBY",
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_PIKE_ROOM_FINAL",
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_PIKE_ROOM_NORMAL",
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_PIKE_ROOM_WILD_MONS",
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_PIKE_THREE_PATH_ROOM",
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_PYRAMID_FLOOR",
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_PYRAMID_LOBBY",
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_PYRAMID_TOP",
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_TOWER_BATTLE_ROOM",
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_TOWER_CORRIDOR",
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_TOWER_ELEVATOR",
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_TOWER_LOBBY",
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_TOWER_MULTI_BATTLE_ROOM",
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_TOWER_MULTI_CORRIDOR",
|
||||
"MAP_BATTLE_FRONTIER_BATTLE_TOWER_MULTI_PARTNER_ROOM",
|
||||
"MAP_BATTLE_FRONTIER_EXCHANGE_SERVICE_CORNER",
|
||||
"MAP_BATTLE_FRONTIER_LOUNGE1",
|
||||
"MAP_BATTLE_FRONTIER_LOUNGE2",
|
||||
"MAP_BATTLE_FRONTIER_LOUNGE3",
|
||||
"MAP_BATTLE_FRONTIER_LOUNGE4",
|
||||
"MAP_BATTLE_FRONTIER_LOUNGE5",
|
||||
"MAP_BATTLE_FRONTIER_LOUNGE6",
|
||||
"MAP_BATTLE_FRONTIER_LOUNGE7",
|
||||
"MAP_BATTLE_FRONTIER_LOUNGE8",
|
||||
"MAP_BATTLE_FRONTIER_LOUNGE9",
|
||||
"MAP_BATTLE_FRONTIER_MART",
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_EAST",
|
||||
"MAP_BATTLE_FRONTIER_OUTSIDE_WEST",
|
||||
"MAP_BATTLE_FRONTIER_POKEMON_CENTER_1F",
|
||||
"MAP_BATTLE_FRONTIER_POKEMON_CENTER_2F",
|
||||
"MAP_BATTLE_FRONTIER_RANKING_HALL",
|
||||
"MAP_BATTLE_FRONTIER_RECEPTION_GATE",
|
||||
"MAP_BATTLE_FRONTIER_SCOTTS_HOUSE",
|
||||
"MAP_BATTLE_PYRAMID_SQUARE01",
|
||||
"MAP_BATTLE_PYRAMID_SQUARE02",
|
||||
"MAP_BATTLE_PYRAMID_SQUARE03",
|
||||
"MAP_BATTLE_PYRAMID_SQUARE04",
|
||||
"MAP_BATTLE_PYRAMID_SQUARE05",
|
||||
"MAP_BATTLE_PYRAMID_SQUARE06",
|
||||
"MAP_BATTLE_PYRAMID_SQUARE07",
|
||||
"MAP_BATTLE_PYRAMID_SQUARE08",
|
||||
"MAP_BATTLE_PYRAMID_SQUARE09",
|
||||
"MAP_BATTLE_PYRAMID_SQUARE10",
|
||||
"MAP_BATTLE_PYRAMID_SQUARE11",
|
||||
"MAP_BATTLE_PYRAMID_SQUARE12",
|
||||
"MAP_BATTLE_PYRAMID_SQUARE13",
|
||||
"MAP_BATTLE_PYRAMID_SQUARE14",
|
||||
"MAP_BATTLE_PYRAMID_SQUARE15",
|
||||
"MAP_BATTLE_PYRAMID_SQUARE16",
|
||||
},
|
||||
"Birth Island": {
|
||||
"MAP_BIRTH_ISLAND_EXTERIOR",
|
||||
"MAP_BIRTH_ISLAND_HARBOR",
|
||||
},
|
||||
"Contest Hall": {
|
||||
"MAP_CONTEST_HALL",
|
||||
"MAP_CONTEST_HALL_BEAUTY",
|
||||
"MAP_CONTEST_HALL_COOL",
|
||||
"MAP_CONTEST_HALL_CUTE",
|
||||
"MAP_CONTEST_HALL_SMART",
|
||||
"MAP_CONTEST_HALL_TOUGH",
|
||||
},
|
||||
"Dewford Town": {
|
||||
"MAP_DEWFORD_TOWN",
|
||||
"MAP_DEWFORD_TOWN_GYM",
|
||||
"MAP_DEWFORD_TOWN_HALL",
|
||||
"MAP_DEWFORD_TOWN_HOUSE1",
|
||||
"MAP_DEWFORD_TOWN_HOUSE2",
|
||||
"MAP_DEWFORD_TOWN_POKEMON_CENTER_1F",
|
||||
"MAP_DEWFORD_TOWN_POKEMON_CENTER_2F",
|
||||
},
|
||||
"Ever Grande City": {
|
||||
"MAP_EVER_GRANDE_CITY",
|
||||
"MAP_EVER_GRANDE_CITY_CHAMPIONS_ROOM",
|
||||
"MAP_EVER_GRANDE_CITY_DRAKES_ROOM",
|
||||
"MAP_EVER_GRANDE_CITY_GLACIAS_ROOM",
|
||||
"MAP_EVER_GRANDE_CITY_HALL1",
|
||||
"MAP_EVER_GRANDE_CITY_HALL2",
|
||||
"MAP_EVER_GRANDE_CITY_HALL3",
|
||||
"MAP_EVER_GRANDE_CITY_HALL4",
|
||||
"MAP_EVER_GRANDE_CITY_HALL5",
|
||||
"MAP_EVER_GRANDE_CITY_HALL_OF_FAME",
|
||||
"MAP_EVER_GRANDE_CITY_PHOEBES_ROOM",
|
||||
"MAP_EVER_GRANDE_CITY_POKEMON_CENTER_1F",
|
||||
"MAP_EVER_GRANDE_CITY_POKEMON_CENTER_2F",
|
||||
"MAP_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F",
|
||||
"MAP_EVER_GRANDE_CITY_POKEMON_LEAGUE_2F",
|
||||
"MAP_EVER_GRANDE_CITY_SIDNEYS_ROOM",
|
||||
},
|
||||
"Fallarbor Town": {
|
||||
"MAP_FALLARBOR_TOWN",
|
||||
"MAP_FALLARBOR_TOWN_BATTLE_TENT_BATTLE_ROOM",
|
||||
"MAP_FALLARBOR_TOWN_BATTLE_TENT_CORRIDOR",
|
||||
"MAP_FALLARBOR_TOWN_BATTLE_TENT_LOBBY",
|
||||
"MAP_FALLARBOR_TOWN_COZMOS_HOUSE",
|
||||
"MAP_FALLARBOR_TOWN_MART",
|
||||
"MAP_FALLARBOR_TOWN_MOVE_RELEARNERS_HOUSE",
|
||||
"MAP_FALLARBOR_TOWN_POKEMON_CENTER_1F",
|
||||
"MAP_FALLARBOR_TOWN_POKEMON_CENTER_2F",
|
||||
},
|
||||
"Faraway Island": {
|
||||
"MAP_FARAWAY_ISLAND_ENTRANCE",
|
||||
"MAP_FARAWAY_ISLAND_INTERIOR",
|
||||
},
|
||||
"Fiery Path": {"MAP_FIERY_PATH"},
|
||||
"Fortree City": {
|
||||
"MAP_FORTREE_CITY",
|
||||
"MAP_FORTREE_CITY_DECORATION_SHOP",
|
||||
"MAP_FORTREE_CITY_GYM",
|
||||
"MAP_FORTREE_CITY_HOUSE1",
|
||||
"MAP_FORTREE_CITY_HOUSE2",
|
||||
"MAP_FORTREE_CITY_HOUSE3",
|
||||
"MAP_FORTREE_CITY_HOUSE4",
|
||||
"MAP_FORTREE_CITY_HOUSE5",
|
||||
"MAP_FORTREE_CITY_MART",
|
||||
"MAP_FORTREE_CITY_POKEMON_CENTER_1F",
|
||||
"MAP_FORTREE_CITY_POKEMON_CENTER_2F",
|
||||
},
|
||||
"Granite Cave": {
|
||||
"MAP_GRANITE_CAVE_1F",
|
||||
"MAP_GRANITE_CAVE_B1F",
|
||||
"MAP_GRANITE_CAVE_B2F",
|
||||
"MAP_GRANITE_CAVE_STEVENS_ROOM",
|
||||
},
|
||||
"Jagged Pass": {"MAP_JAGGED_PASS"},
|
||||
"Lavaridge Town": {
|
||||
"MAP_LAVARIDGE_TOWN",
|
||||
"MAP_LAVARIDGE_TOWN_GYM_1F",
|
||||
"MAP_LAVARIDGE_TOWN_GYM_B1F",
|
||||
"MAP_LAVARIDGE_TOWN_HERB_SHOP",
|
||||
"MAP_LAVARIDGE_TOWN_HOUSE",
|
||||
"MAP_LAVARIDGE_TOWN_MART",
|
||||
"MAP_LAVARIDGE_TOWN_POKEMON_CENTER_1F",
|
||||
"MAP_LAVARIDGE_TOWN_POKEMON_CENTER_2F",
|
||||
},
|
||||
"Lilycove City": {
|
||||
"MAP_LILYCOVE_CITY",
|
||||
"MAP_LILYCOVE_CITY_CONTEST_HALL",
|
||||
"MAP_LILYCOVE_CITY_CONTEST_LOBBY",
|
||||
"MAP_LILYCOVE_CITY_COVE_LILY_MOTEL_1F",
|
||||
"MAP_LILYCOVE_CITY_COVE_LILY_MOTEL_2F",
|
||||
"MAP_LILYCOVE_CITY_DEPARTMENT_STORE_1F",
|
||||
"MAP_LILYCOVE_CITY_DEPARTMENT_STORE_2F",
|
||||
"MAP_LILYCOVE_CITY_DEPARTMENT_STORE_3F",
|
||||
"MAP_LILYCOVE_CITY_DEPARTMENT_STORE_4F",
|
||||
"MAP_LILYCOVE_CITY_DEPARTMENT_STORE_5F",
|
||||
"MAP_LILYCOVE_CITY_DEPARTMENT_STORE_ELEVATOR",
|
||||
"MAP_LILYCOVE_CITY_DEPARTMENT_STORE_ROOFTOP",
|
||||
"MAP_LILYCOVE_CITY_HARBOR",
|
||||
"MAP_LILYCOVE_CITY_HOUSE1",
|
||||
"MAP_LILYCOVE_CITY_HOUSE2",
|
||||
"MAP_LILYCOVE_CITY_HOUSE3",
|
||||
"MAP_LILYCOVE_CITY_HOUSE4",
|
||||
"MAP_LILYCOVE_CITY_LILYCOVE_MUSEUM_1F",
|
||||
"MAP_LILYCOVE_CITY_LILYCOVE_MUSEUM_2F",
|
||||
"MAP_LILYCOVE_CITY_MOVE_DELETERS_HOUSE",
|
||||
"MAP_LILYCOVE_CITY_POKEMON_CENTER_1F",
|
||||
"MAP_LILYCOVE_CITY_POKEMON_CENTER_2F",
|
||||
"MAP_LILYCOVE_CITY_POKEMON_TRAINER_FAN_CLUB",
|
||||
},
|
||||
"Littleroot Town": {
|
||||
"MAP_INSIDE_OF_TRUCK",
|
||||
"MAP_LITTLEROOT_TOWN",
|
||||
"MAP_LITTLEROOT_TOWN_BRENDANS_HOUSE_1F",
|
||||
"MAP_LITTLEROOT_TOWN_BRENDANS_HOUSE_2F",
|
||||
"MAP_LITTLEROOT_TOWN_MAYS_HOUSE_1F",
|
||||
"MAP_LITTLEROOT_TOWN_MAYS_HOUSE_2F",
|
||||
"MAP_LITTLEROOT_TOWN_PROFESSOR_BIRCHS_LAB",
|
||||
},
|
||||
"Magma Hideout": {
|
||||
"MAP_MAGMA_HIDEOUT_1F",
|
||||
"MAP_MAGMA_HIDEOUT_2F_1R",
|
||||
"MAP_MAGMA_HIDEOUT_2F_2R",
|
||||
"MAP_MAGMA_HIDEOUT_2F_3R",
|
||||
"MAP_MAGMA_HIDEOUT_3F_1R",
|
||||
"MAP_MAGMA_HIDEOUT_3F_2R",
|
||||
"MAP_MAGMA_HIDEOUT_3F_3R",
|
||||
"MAP_MAGMA_HIDEOUT_4F",
|
||||
},
|
||||
"Marine Cave": {
|
||||
"MAP_MARINE_CAVE_END",
|
||||
"MAP_MARINE_CAVE_ENTRANCE",
|
||||
"MAP_UNDERWATER_MARINE_CAVE",
|
||||
},
|
||||
"Mauville City": {
|
||||
"MAP_MAUVILLE_CITY",
|
||||
"MAP_MAUVILLE_CITY_BIKE_SHOP",
|
||||
"MAP_MAUVILLE_CITY_GAME_CORNER",
|
||||
"MAP_MAUVILLE_CITY_GYM",
|
||||
"MAP_MAUVILLE_CITY_HOUSE1",
|
||||
"MAP_MAUVILLE_CITY_HOUSE2",
|
||||
"MAP_MAUVILLE_CITY_MART",
|
||||
"MAP_MAUVILLE_CITY_POKEMON_CENTER_1F",
|
||||
"MAP_MAUVILLE_CITY_POKEMON_CENTER_2F",
|
||||
},
|
||||
"Meteor Falls": {
|
||||
"MAP_METEOR_FALLS_1F_1R",
|
||||
"MAP_METEOR_FALLS_1F_2R",
|
||||
"MAP_METEOR_FALLS_B1F_1R",
|
||||
"MAP_METEOR_FALLS_B1F_2R",
|
||||
"MAP_METEOR_FALLS_STEVENS_CAVE",
|
||||
},
|
||||
"Mirage Tower": {
|
||||
"MAP_MIRAGE_TOWER_1F",
|
||||
"MAP_MIRAGE_TOWER_2F",
|
||||
"MAP_MIRAGE_TOWER_3F",
|
||||
"MAP_MIRAGE_TOWER_4F",
|
||||
},
|
||||
"Mossdeep City": {
|
||||
"MAP_MOSSDEEP_CITY",
|
||||
"MAP_MOSSDEEP_CITY_GAME_CORNER_1F",
|
||||
"MAP_MOSSDEEP_CITY_GAME_CORNER_B1F",
|
||||
"MAP_MOSSDEEP_CITY_GYM",
|
||||
"MAP_MOSSDEEP_CITY_HOUSE1",
|
||||
"MAP_MOSSDEEP_CITY_HOUSE2",
|
||||
"MAP_MOSSDEEP_CITY_HOUSE3",
|
||||
"MAP_MOSSDEEP_CITY_HOUSE4",
|
||||
"MAP_MOSSDEEP_CITY_MART",
|
||||
"MAP_MOSSDEEP_CITY_POKEMON_CENTER_1F",
|
||||
"MAP_MOSSDEEP_CITY_POKEMON_CENTER_2F",
|
||||
"MAP_MOSSDEEP_CITY_SPACE_CENTER_1F",
|
||||
"MAP_MOSSDEEP_CITY_SPACE_CENTER_2F",
|
||||
"MAP_MOSSDEEP_CITY_STEVENS_HOUSE",
|
||||
},
|
||||
"Mt. Chimney": {
|
||||
"MAP_MT_CHIMNEY",
|
||||
"MAP_MT_CHIMNEY_CABLE_CAR_STATION",
|
||||
},
|
||||
"Mt. Pyre": {
|
||||
"MAP_MT_PYRE_1F",
|
||||
"MAP_MT_PYRE_2F",
|
||||
"MAP_MT_PYRE_3F",
|
||||
"MAP_MT_PYRE_4F",
|
||||
"MAP_MT_PYRE_5F",
|
||||
"MAP_MT_PYRE_6F",
|
||||
"MAP_MT_PYRE_EXTERIOR",
|
||||
"MAP_MT_PYRE_SUMMIT",
|
||||
},
|
||||
"Navel Rock": {
|
||||
"MAP_NAVEL_ROCK_B1F",
|
||||
"MAP_NAVEL_ROCK_BOTTOM",
|
||||
"MAP_NAVEL_ROCK_DOWN01",
|
||||
"MAP_NAVEL_ROCK_DOWN02",
|
||||
"MAP_NAVEL_ROCK_DOWN03",
|
||||
"MAP_NAVEL_ROCK_DOWN04",
|
||||
"MAP_NAVEL_ROCK_DOWN05",
|
||||
"MAP_NAVEL_ROCK_DOWN06",
|
||||
"MAP_NAVEL_ROCK_DOWN07",
|
||||
"MAP_NAVEL_ROCK_DOWN08",
|
||||
"MAP_NAVEL_ROCK_DOWN09",
|
||||
"MAP_NAVEL_ROCK_DOWN10",
|
||||
"MAP_NAVEL_ROCK_DOWN11",
|
||||
"MAP_NAVEL_ROCK_ENTRANCE",
|
||||
"MAP_NAVEL_ROCK_EXTERIOR",
|
||||
"MAP_NAVEL_ROCK_FORK",
|
||||
"MAP_NAVEL_ROCK_HARBOR",
|
||||
"MAP_NAVEL_ROCK_TOP",
|
||||
"MAP_NAVEL_ROCK_UP1",
|
||||
"MAP_NAVEL_ROCK_UP2",
|
||||
"MAP_NAVEL_ROCK_UP3",
|
||||
"MAP_NAVEL_ROCK_UP4",
|
||||
},
|
||||
"New Mauville": {
|
||||
"MAP_NEW_MAUVILLE_ENTRANCE",
|
||||
"MAP_NEW_MAUVILLE_INSIDE",
|
||||
},
|
||||
"Oldale Town": {
|
||||
"MAP_OLDALE_TOWN",
|
||||
"MAP_OLDALE_TOWN_HOUSE1",
|
||||
"MAP_OLDALE_TOWN_HOUSE2",
|
||||
"MAP_OLDALE_TOWN_MART",
|
||||
"MAP_OLDALE_TOWN_POKEMON_CENTER_1F",
|
||||
"MAP_OLDALE_TOWN_POKEMON_CENTER_2F",
|
||||
},
|
||||
"Pacifidlog Town": {
|
||||
"MAP_PACIFIDLOG_TOWN",
|
||||
"MAP_PACIFIDLOG_TOWN_HOUSE1",
|
||||
"MAP_PACIFIDLOG_TOWN_HOUSE2",
|
||||
"MAP_PACIFIDLOG_TOWN_HOUSE3",
|
||||
"MAP_PACIFIDLOG_TOWN_HOUSE4",
|
||||
"MAP_PACIFIDLOG_TOWN_HOUSE5",
|
||||
"MAP_PACIFIDLOG_TOWN_POKEMON_CENTER_1F",
|
||||
"MAP_PACIFIDLOG_TOWN_POKEMON_CENTER_2F",
|
||||
},
|
||||
"Petalburg City": {
|
||||
"MAP_PETALBURG_CITY",
|
||||
"MAP_PETALBURG_CITY_GYM",
|
||||
"MAP_PETALBURG_CITY_HOUSE1",
|
||||
"MAP_PETALBURG_CITY_HOUSE2",
|
||||
"MAP_PETALBURG_CITY_MART",
|
||||
"MAP_PETALBURG_CITY_POKEMON_CENTER_1F",
|
||||
"MAP_PETALBURG_CITY_POKEMON_CENTER_2F",
|
||||
"MAP_PETALBURG_CITY_WALLYS_HOUSE",
|
||||
},
|
||||
"Petalburg Woods": {"MAP_PETALBURG_WOODS"},
|
||||
"Route 101": {"MAP_ROUTE101"},
|
||||
"Route 102": {"MAP_ROUTE102"},
|
||||
"Route 103": {"MAP_ROUTE103"},
|
||||
"Route 104": {
|
||||
"MAP_ROUTE104",
|
||||
"MAP_ROUTE104_MR_BRINEYS_HOUSE",
|
||||
"MAP_ROUTE104_PRETTY_PETAL_FLOWER_SHOP",
|
||||
},
|
||||
"Route 105": {
|
||||
"MAP_ISLAND_CAVE",
|
||||
"MAP_ROUTE105",
|
||||
"MAP_UNDERWATER_ROUTE105",
|
||||
},
|
||||
"Route 106": {"MAP_ROUTE106"},
|
||||
"Route 107": {"MAP_ROUTE107"},
|
||||
"Route 108": {"MAP_ROUTE108"},
|
||||
"Route 109": {
|
||||
"MAP_ROUTE109",
|
||||
"MAP_ROUTE109_SEASHORE_HOUSE",
|
||||
},
|
||||
"Route 110": {
|
||||
"MAP_ROUTE110",
|
||||
"MAP_ROUTE110_SEASIDE_CYCLING_ROAD_NORTH_ENTRANCE",
|
||||
"MAP_ROUTE110_SEASIDE_CYCLING_ROAD_SOUTH_ENTRANCE",
|
||||
},
|
||||
"Trick House": {
|
||||
"MAP_ROUTE110_TRICK_HOUSE_CORRIDOR",
|
||||
"MAP_ROUTE110_TRICK_HOUSE_END",
|
||||
"MAP_ROUTE110_TRICK_HOUSE_ENTRANCE",
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE1",
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE2",
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE3",
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE4",
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE5",
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE6",
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE7",
|
||||
"MAP_ROUTE110_TRICK_HOUSE_PUZZLE8",
|
||||
},
|
||||
"Route 111": {
|
||||
"MAP_DESERT_RUINS",
|
||||
"MAP_ROUTE111",
|
||||
"MAP_ROUTE111_OLD_LADYS_REST_STOP",
|
||||
"MAP_ROUTE111_WINSTRATE_FAMILYS_HOUSE",
|
||||
},
|
||||
"Route 112": {
|
||||
"MAP_ROUTE112",
|
||||
"MAP_ROUTE112_CABLE_CAR_STATION",
|
||||
},
|
||||
"Route 113": {
|
||||
"MAP_ROUTE113",
|
||||
"MAP_ROUTE113_GLASS_WORKSHOP",
|
||||
},
|
||||
"Route 114": {
|
||||
"MAP_DESERT_UNDERPASS",
|
||||
"MAP_ROUTE114",
|
||||
"MAP_ROUTE114_FOSSIL_MANIACS_HOUSE",
|
||||
"MAP_ROUTE114_FOSSIL_MANIACS_TUNNEL",
|
||||
"MAP_ROUTE114_LANETTES_HOUSE",
|
||||
},
|
||||
"Route 115": {"MAP_ROUTE115"},
|
||||
"Route 116": {
|
||||
"MAP_ROUTE116",
|
||||
"MAP_ROUTE116_TUNNELERS_REST_HOUSE",
|
||||
},
|
||||
"Route 117": {
|
||||
"MAP_ROUTE117",
|
||||
"MAP_ROUTE117_POKEMON_DAY_CARE",
|
||||
},
|
||||
"Route 118": {"MAP_ROUTE118"},
|
||||
"Route 119": {
|
||||
"MAP_ROUTE119",
|
||||
"MAP_ROUTE119_HOUSE",
|
||||
"MAP_ROUTE119_WEATHER_INSTITUTE_1F",
|
||||
"MAP_ROUTE119_WEATHER_INSTITUTE_2F",
|
||||
},
|
||||
"Route 120": {
|
||||
"MAP_ANCIENT_TOMB",
|
||||
"MAP_ROUTE120",
|
||||
"MAP_SCORCHED_SLAB",
|
||||
},
|
||||
"Route 121": {
|
||||
"MAP_ROUTE121",
|
||||
},
|
||||
"Route 122": {"MAP_ROUTE122"},
|
||||
"Route 123": {
|
||||
"MAP_ROUTE123",
|
||||
"MAP_ROUTE123_BERRY_MASTERS_HOUSE",
|
||||
},
|
||||
"Route 124": {
|
||||
"MAP_ROUTE124",
|
||||
"MAP_ROUTE124_DIVING_TREASURE_HUNTERS_HOUSE",
|
||||
"MAP_UNDERWATER_ROUTE124",
|
||||
},
|
||||
"Route 125": {
|
||||
"MAP_ROUTE125",
|
||||
"MAP_UNDERWATER_ROUTE125",
|
||||
},
|
||||
"Route 126": {
|
||||
"MAP_ROUTE126",
|
||||
"MAP_UNDERWATER_ROUTE126",
|
||||
},
|
||||
"Route 127": {
|
||||
"MAP_ROUTE127",
|
||||
"MAP_UNDERWATER_ROUTE127",
|
||||
},
|
||||
"Route 128": {
|
||||
"MAP_ROUTE128",
|
||||
"MAP_UNDERWATER_ROUTE128",
|
||||
},
|
||||
"Route 129": {
|
||||
"MAP_ROUTE129",
|
||||
"MAP_UNDERWATER_ROUTE129",
|
||||
},
|
||||
"Route 130": {"MAP_ROUTE130"},
|
||||
"Route 131": {"MAP_ROUTE131"},
|
||||
"Route 132": {"MAP_ROUTE132"},
|
||||
"Route 133": {"MAP_ROUTE133"},
|
||||
"Route 134": {
|
||||
"MAP_ROUTE134",
|
||||
"MAP_UNDERWATER_ROUTE134",
|
||||
"MAP_SEALED_CHAMBER_INNER_ROOM",
|
||||
"MAP_SEALED_CHAMBER_OUTER_ROOM",
|
||||
"MAP_UNDERWATER_SEALED_CHAMBER",
|
||||
},
|
||||
"Rustboro City": {
|
||||
"MAP_RUSTBORO_CITY",
|
||||
"MAP_RUSTBORO_CITY_CUTTERS_HOUSE",
|
||||
"MAP_RUSTBORO_CITY_DEVON_CORP_1F",
|
||||
"MAP_RUSTBORO_CITY_DEVON_CORP_2F",
|
||||
"MAP_RUSTBORO_CITY_DEVON_CORP_3F",
|
||||
"MAP_RUSTBORO_CITY_FLAT1_1F",
|
||||
"MAP_RUSTBORO_CITY_FLAT1_2F",
|
||||
"MAP_RUSTBORO_CITY_FLAT2_1F",
|
||||
"MAP_RUSTBORO_CITY_FLAT2_2F",
|
||||
"MAP_RUSTBORO_CITY_FLAT2_3F",
|
||||
"MAP_RUSTBORO_CITY_GYM",
|
||||
"MAP_RUSTBORO_CITY_HOUSE1",
|
||||
"MAP_RUSTBORO_CITY_HOUSE2",
|
||||
"MAP_RUSTBORO_CITY_HOUSE3",
|
||||
"MAP_RUSTBORO_CITY_MART",
|
||||
"MAP_RUSTBORO_CITY_POKEMON_CENTER_1F",
|
||||
"MAP_RUSTBORO_CITY_POKEMON_CENTER_2F",
|
||||
"MAP_RUSTBORO_CITY_POKEMON_SCHOOL",
|
||||
},
|
||||
"Rusturf Tunnel": {"MAP_RUSTURF_TUNNEL"},
|
||||
"Safari Zone": {
|
||||
"MAP_ROUTE121_SAFARI_ZONE_ENTRANCE",
|
||||
"MAP_SAFARI_ZONE_NORTH",
|
||||
"MAP_SAFARI_ZONE_NORTHEAST",
|
||||
"MAP_SAFARI_ZONE_NORTHWEST",
|
||||
"MAP_SAFARI_ZONE_REST_HOUSE",
|
||||
"MAP_SAFARI_ZONE_SOUTH",
|
||||
"MAP_SAFARI_ZONE_SOUTHEAST",
|
||||
"MAP_SAFARI_ZONE_SOUTHWEST",
|
||||
},
|
||||
"Seafloor Cavern": {
|
||||
"MAP_SEAFLOOR_CAVERN_ENTRANCE",
|
||||
"MAP_SEAFLOOR_CAVERN_ROOM1",
|
||||
"MAP_SEAFLOOR_CAVERN_ROOM2",
|
||||
"MAP_SEAFLOOR_CAVERN_ROOM3",
|
||||
"MAP_SEAFLOOR_CAVERN_ROOM4",
|
||||
"MAP_SEAFLOOR_CAVERN_ROOM5",
|
||||
"MAP_SEAFLOOR_CAVERN_ROOM6",
|
||||
"MAP_SEAFLOOR_CAVERN_ROOM7",
|
||||
"MAP_SEAFLOOR_CAVERN_ROOM8",
|
||||
"MAP_SEAFLOOR_CAVERN_ROOM9",
|
||||
"MAP_UNDERWATER_SEAFLOOR_CAVERN",
|
||||
},
|
||||
"Shoal Cave": {
|
||||
"MAP_SHOAL_CAVE_HIGH_TIDE_ENTRANCE_ROOM",
|
||||
"MAP_SHOAL_CAVE_HIGH_TIDE_INNER_ROOM",
|
||||
"MAP_SHOAL_CAVE_LOW_TIDE_ENTRANCE_ROOM",
|
||||
"MAP_SHOAL_CAVE_LOW_TIDE_ICE_ROOM",
|
||||
"MAP_SHOAL_CAVE_LOW_TIDE_INNER_ROOM",
|
||||
"MAP_SHOAL_CAVE_LOW_TIDE_LOWER_ROOM",
|
||||
"MAP_SHOAL_CAVE_LOW_TIDE_STAIRS_ROOM",
|
||||
},
|
||||
"Sky Pillar": {
|
||||
"MAP_SKY_PILLAR_1F",
|
||||
"MAP_SKY_PILLAR_2F",
|
||||
"MAP_SKY_PILLAR_3F",
|
||||
"MAP_SKY_PILLAR_4F",
|
||||
"MAP_SKY_PILLAR_5F",
|
||||
"MAP_SKY_PILLAR_ENTRANCE",
|
||||
"MAP_SKY_PILLAR_OUTSIDE",
|
||||
"MAP_SKY_PILLAR_TOP",
|
||||
},
|
||||
"Slateport City": {
|
||||
"MAP_SLATEPORT_CITY",
|
||||
"MAP_SLATEPORT_CITY_BATTLE_TENT_BATTLE_ROOM",
|
||||
"MAP_SLATEPORT_CITY_BATTLE_TENT_CORRIDOR",
|
||||
"MAP_SLATEPORT_CITY_BATTLE_TENT_LOBBY",
|
||||
"MAP_SLATEPORT_CITY_HARBOR",
|
||||
"MAP_SLATEPORT_CITY_HOUSE",
|
||||
"MAP_SLATEPORT_CITY_MART",
|
||||
"MAP_SLATEPORT_CITY_NAME_RATERS_HOUSE",
|
||||
"MAP_SLATEPORT_CITY_OCEANIC_MUSEUM_1F",
|
||||
"MAP_SLATEPORT_CITY_OCEANIC_MUSEUM_2F",
|
||||
"MAP_SLATEPORT_CITY_POKEMON_CENTER_1F",
|
||||
"MAP_SLATEPORT_CITY_POKEMON_CENTER_2F",
|
||||
"MAP_SLATEPORT_CITY_POKEMON_FAN_CLUB",
|
||||
"MAP_SLATEPORT_CITY_STERNS_SHIPYARD_1F",
|
||||
"MAP_SLATEPORT_CITY_STERNS_SHIPYARD_2F",
|
||||
},
|
||||
"Sootopolis City": {
|
||||
"MAP_CAVE_OF_ORIGIN_1F",
|
||||
"MAP_CAVE_OF_ORIGIN_B1F",
|
||||
"MAP_CAVE_OF_ORIGIN_ENTRANCE",
|
||||
"MAP_SOOTOPOLIS_CITY",
|
||||
"MAP_SOOTOPOLIS_CITY_GYM_1F",
|
||||
"MAP_SOOTOPOLIS_CITY_GYM_B1F",
|
||||
"MAP_SOOTOPOLIS_CITY_HOUSE1",
|
||||
"MAP_SOOTOPOLIS_CITY_HOUSE2",
|
||||
"MAP_SOOTOPOLIS_CITY_HOUSE3",
|
||||
"MAP_SOOTOPOLIS_CITY_HOUSE4",
|
||||
"MAP_SOOTOPOLIS_CITY_HOUSE5",
|
||||
"MAP_SOOTOPOLIS_CITY_HOUSE6",
|
||||
"MAP_SOOTOPOLIS_CITY_HOUSE7",
|
||||
"MAP_SOOTOPOLIS_CITY_LOTAD_AND_SEEDOT_HOUSE",
|
||||
"MAP_SOOTOPOLIS_CITY_MART",
|
||||
"MAP_SOOTOPOLIS_CITY_MYSTERY_EVENTS_HOUSE_1F",
|
||||
"MAP_SOOTOPOLIS_CITY_MYSTERY_EVENTS_HOUSE_B1F",
|
||||
"MAP_SOOTOPOLIS_CITY_POKEMON_CENTER_1F",
|
||||
"MAP_SOOTOPOLIS_CITY_POKEMON_CENTER_2F",
|
||||
"MAP_UNDERWATER_SOOTOPOLIS_CITY",
|
||||
},
|
||||
"Southern Island": {
|
||||
"MAP_SOUTHERN_ISLAND_EXTERIOR",
|
||||
"MAP_SOUTHERN_ISLAND_INTERIOR",
|
||||
},
|
||||
"S.S. Tidal": {
|
||||
"MAP_SS_TIDAL_CORRIDOR",
|
||||
"MAP_SS_TIDAL_LOWER_DECK",
|
||||
"MAP_SS_TIDAL_ROOMS",
|
||||
},
|
||||
"Terra Cave": {
|
||||
"MAP_TERRA_CAVE_END",
|
||||
"MAP_TERRA_CAVE_ENTRANCE",
|
||||
},
|
||||
"Trainer Hill": {
|
||||
"MAP_TRAINER_HILL_2F",
|
||||
"MAP_TRAINER_HILL_3F",
|
||||
"MAP_TRAINER_HILL_4F",
|
||||
"MAP_TRAINER_HILL_ELEVATOR",
|
||||
"MAP_TRAINER_HILL_ENTRANCE",
|
||||
"MAP_TRAINER_HILL_ROOF",
|
||||
},
|
||||
"Verdanturf Town": {
|
||||
"MAP_VERDANTURF_TOWN",
|
||||
"MAP_VERDANTURF_TOWN_BATTLE_TENT_BATTLE_ROOM",
|
||||
"MAP_VERDANTURF_TOWN_BATTLE_TENT_CORRIDOR",
|
||||
"MAP_VERDANTURF_TOWN_BATTLE_TENT_LOBBY",
|
||||
"MAP_VERDANTURF_TOWN_FRIENDSHIP_RATERS_HOUSE",
|
||||
"MAP_VERDANTURF_TOWN_HOUSE",
|
||||
"MAP_VERDANTURF_TOWN_MART",
|
||||
"MAP_VERDANTURF_TOWN_POKEMON_CENTER_1F",
|
||||
"MAP_VERDANTURF_TOWN_POKEMON_CENTER_2F",
|
||||
"MAP_VERDANTURF_TOWN_WANDAS_HOUSE",
|
||||
},
|
||||
"Victory Road": {
|
||||
"MAP_VICTORY_ROAD_1F",
|
||||
"MAP_VICTORY_ROAD_B1F",
|
||||
"MAP_VICTORY_ROAD_B2F",
|
||||
},
|
||||
}
|
||||
|
||||
_LOCATION_CATEGORY_TO_GROUP_NAME = {
|
||||
LocationCategory.BADGE: "Badges",
|
||||
LocationCategory.HM: "HMs",
|
||||
LocationCategory.KEY: "Key Items",
|
||||
LocationCategory.ROD: "Fishing Rods",
|
||||
LocationCategory.BIKE: "Bikes",
|
||||
LocationCategory.TICKET: "Tickets",
|
||||
LocationCategory.OVERWORLD_ITEM: "Overworld Items",
|
||||
LocationCategory.HIDDEN_ITEM: "Hidden Items",
|
||||
LocationCategory.GIFT: "NPC Gifts",
|
||||
LocationCategory.BERRY_TREE: "Berry Trees",
|
||||
LocationCategory.TRAINER: "Trainers",
|
||||
LocationCategory.POKEDEX: "Pokedex",
|
||||
}
|
||||
|
||||
LOCATION_GROUPS: Dict[str, Set[str]] = {group_name: set() for group_name in _LOCATION_CATEGORY_TO_GROUP_NAME.values()}
|
||||
for location in data.locations.values():
|
||||
# Category groups
|
||||
LOCATION_GROUPS[_LOCATION_CATEGORY_TO_GROUP_NAME[location.category]].add(location.label)
|
||||
|
||||
# Tag groups
|
||||
for tag in location.tags:
|
||||
if tag not in LOCATION_GROUPS:
|
||||
LOCATION_GROUPS[tag] = set()
|
||||
LOCATION_GROUPS[tag].add(location.label)
|
||||
|
||||
# Geographic groups
|
||||
if location.parent_region != "REGION_POKEDEX":
|
||||
map_name = data.regions[location.parent_region].parent_map.name
|
||||
for group, maps in _LOCATION_GROUP_MAPS.items():
|
||||
if map_name in maps:
|
||||
if group not in LOCATION_GROUPS:
|
||||
LOCATION_GROUPS[group] = set()
|
||||
LOCATION_GROUPS[group].add(location.label)
|
||||
break
|
||||
|
||||
# Meta-groups
|
||||
LOCATION_GROUPS["Cities"] = {
|
||||
*LOCATION_GROUPS.get("Littleroot Town", set()),
|
||||
*LOCATION_GROUPS.get("Oldale Town", set()),
|
||||
*LOCATION_GROUPS.get("Petalburg City", set()),
|
||||
*LOCATION_GROUPS.get("Rustboro City", set()),
|
||||
*LOCATION_GROUPS.get("Dewford Town", set()),
|
||||
*LOCATION_GROUPS.get("Slateport City", set()),
|
||||
*LOCATION_GROUPS.get("Mauville City", set()),
|
||||
*LOCATION_GROUPS.get("Verdanturf Town", set()),
|
||||
*LOCATION_GROUPS.get("Fallarbor Town", set()),
|
||||
*LOCATION_GROUPS.get("Lavaridge Town", set()),
|
||||
*LOCATION_GROUPS.get("Fortree City", set()),
|
||||
*LOCATION_GROUPS.get("Mossdeep City", set()),
|
||||
*LOCATION_GROUPS.get("Sootopolis City", set()),
|
||||
*LOCATION_GROUPS.get("Pacifidlog Town", set()),
|
||||
*LOCATION_GROUPS.get("Ever Grande City", set()),
|
||||
}
|
||||
|
||||
LOCATION_GROUPS["Dungeons"] = {
|
||||
*LOCATION_GROUPS.get("Petalburg Woods", set()),
|
||||
*LOCATION_GROUPS.get("Rusturf Tunnel", set()),
|
||||
*LOCATION_GROUPS.get("Granite Cave", set()),
|
||||
*LOCATION_GROUPS.get("Fiery Path", set()),
|
||||
*LOCATION_GROUPS.get("Meteor Falls", set()),
|
||||
*LOCATION_GROUPS.get("Jagged Pass", set()),
|
||||
*LOCATION_GROUPS.get("Mt. Chimney", set()),
|
||||
*LOCATION_GROUPS.get("Abandoned Ship", set()),
|
||||
*LOCATION_GROUPS.get("New Mauville", set()),
|
||||
*LOCATION_GROUPS.get("Mt. Pyre", set()),
|
||||
*LOCATION_GROUPS.get("Seafloor Cavern", set()),
|
||||
*LOCATION_GROUPS.get("Sky Pillar", set()),
|
||||
*LOCATION_GROUPS.get("Victory Road", set()),
|
||||
}
|
||||
|
||||
LOCATION_GROUPS["Routes"] = {
|
||||
*LOCATION_GROUPS.get("Route 101", set()),
|
||||
*LOCATION_GROUPS.get("Route 102", set()),
|
||||
*LOCATION_GROUPS.get("Route 103", set()),
|
||||
*LOCATION_GROUPS.get("Route 104", set()),
|
||||
*LOCATION_GROUPS.get("Route 105", set()),
|
||||
*LOCATION_GROUPS.get("Route 106", set()),
|
||||
*LOCATION_GROUPS.get("Route 107", set()),
|
||||
*LOCATION_GROUPS.get("Route 108", set()),
|
||||
*LOCATION_GROUPS.get("Route 109", set()),
|
||||
*LOCATION_GROUPS.get("Route 110", set()),
|
||||
*LOCATION_GROUPS.get("Route 111", set()),
|
||||
*LOCATION_GROUPS.get("Route 112", set()),
|
||||
*LOCATION_GROUPS.get("Route 113", set()),
|
||||
*LOCATION_GROUPS.get("Route 114", set()),
|
||||
*LOCATION_GROUPS.get("Route 115", set()),
|
||||
*LOCATION_GROUPS.get("Route 116", set()),
|
||||
*LOCATION_GROUPS.get("Route 117", set()),
|
||||
*LOCATION_GROUPS.get("Route 118", set()),
|
||||
*LOCATION_GROUPS.get("Route 119", set()),
|
||||
*LOCATION_GROUPS.get("Route 120", set()),
|
||||
*LOCATION_GROUPS.get("Route 121", set()),
|
||||
*LOCATION_GROUPS.get("Route 122", set()),
|
||||
*LOCATION_GROUPS.get("Route 123", set()),
|
||||
*LOCATION_GROUPS.get("Route 124", set()),
|
||||
*LOCATION_GROUPS.get("Route 125", set()),
|
||||
*LOCATION_GROUPS.get("Route 126", set()),
|
||||
*LOCATION_GROUPS.get("Route 127", set()),
|
||||
*LOCATION_GROUPS.get("Route 128", set()),
|
||||
*LOCATION_GROUPS.get("Route 129", set()),
|
||||
*LOCATION_GROUPS.get("Route 130", set()),
|
||||
*LOCATION_GROUPS.get("Route 131", set()),
|
||||
*LOCATION_GROUPS.get("Route 132", set()),
|
||||
*LOCATION_GROUPS.get("Route 133", set()),
|
||||
*LOCATION_GROUPS.get("Route 134", set()),
|
||||
}
|
||||
@@ -1,7 +1,7 @@
|
||||
"""
|
||||
Classes and functions related to AP items for Pokemon Emerald
|
||||
"""
|
||||
from typing import Dict, FrozenSet, Optional
|
||||
from typing import Dict, FrozenSet, Set, Optional
|
||||
|
||||
from BaseClasses import Item, ItemClassification
|
||||
|
||||
@@ -46,30 +46,6 @@ def create_item_label_to_code_map() -> Dict[str, int]:
|
||||
return label_to_code_map
|
||||
|
||||
|
||||
ITEM_GROUPS = {
|
||||
"Badges": {
|
||||
"Stone Badge", "Knuckle Badge",
|
||||
"Dynamo Badge", "Heat Badge",
|
||||
"Balance Badge", "Feather Badge",
|
||||
"Mind Badge", "Rain Badge",
|
||||
},
|
||||
"HMs": {
|
||||
"HM01 Cut", "HM02 Fly",
|
||||
"HM03 Surf", "HM04 Strength",
|
||||
"HM05 Flash", "HM06 Rock Smash",
|
||||
"HM07 Waterfall", "HM08 Dive",
|
||||
},
|
||||
"HM01": {"HM01 Cut"},
|
||||
"HM02": {"HM02 Fly"},
|
||||
"HM03": {"HM03 Surf"},
|
||||
"HM04": {"HM04 Strength"},
|
||||
"HM05": {"HM05 Flash"},
|
||||
"HM06": {"HM06 Rock Smash"},
|
||||
"HM07": {"HM07 Waterfall"},
|
||||
"HM08": {"HM08 Dive"},
|
||||
}
|
||||
|
||||
|
||||
def get_item_classification(item_code: int) -> ItemClassification:
|
||||
"""
|
||||
Returns the item classification for a given AP item id (code)
|
||||
|
||||
@@ -1,59 +1,17 @@
|
||||
"""
|
||||
Classes and functions related to AP locations for Pokemon Emerald
|
||||
"""
|
||||
from typing import TYPE_CHECKING, Dict, Optional, FrozenSet, Iterable
|
||||
from typing import TYPE_CHECKING, Dict, Optional, Set
|
||||
|
||||
from BaseClasses import Location, Region
|
||||
|
||||
from .data import BASE_OFFSET, NATIONAL_ID_TO_SPECIES_ID, POKEDEX_OFFSET, data
|
||||
from .data import BASE_OFFSET, NATIONAL_ID_TO_SPECIES_ID, POKEDEX_OFFSET, LocationCategory, data
|
||||
from .items import offset_item_value
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import PokemonEmeraldWorld
|
||||
|
||||
|
||||
LOCATION_GROUPS = {
|
||||
"Badges": {
|
||||
"Rustboro Gym - Stone Badge",
|
||||
"Dewford Gym - Knuckle Badge",
|
||||
"Mauville Gym - Dynamo Badge",
|
||||
"Lavaridge Gym - Heat Badge",
|
||||
"Petalburg Gym - Balance Badge",
|
||||
"Fortree Gym - Feather Badge",
|
||||
"Mossdeep Gym - Mind Badge",
|
||||
"Sootopolis Gym - Rain Badge",
|
||||
},
|
||||
"Gym TMs": {
|
||||
"Rustboro Gym - TM39 from Roxanne",
|
||||
"Dewford Gym - TM08 from Brawly",
|
||||
"Mauville Gym - TM34 from Wattson",
|
||||
"Lavaridge Gym - TM50 from Flannery",
|
||||
"Petalburg Gym - TM42 from Norman",
|
||||
"Fortree Gym - TM40 from Winona",
|
||||
"Mossdeep Gym - TM04 from Tate and Liza",
|
||||
"Sootopolis Gym - TM03 from Juan",
|
||||
},
|
||||
"Trick House": {
|
||||
"Trick House Puzzle 1 - Item",
|
||||
"Trick House Puzzle 2 - Item 1",
|
||||
"Trick House Puzzle 2 - Item 2",
|
||||
"Trick House Puzzle 3 - Item 1",
|
||||
"Trick House Puzzle 3 - Item 2",
|
||||
"Trick House Puzzle 4 - Item",
|
||||
"Trick House Puzzle 6 - Item",
|
||||
"Trick House Puzzle 7 - Item",
|
||||
"Trick House Puzzle 8 - Item",
|
||||
"Trick House Puzzle 1 - Reward",
|
||||
"Trick House Puzzle 2 - Reward",
|
||||
"Trick House Puzzle 3 - Reward",
|
||||
"Trick House Puzzle 4 - Reward",
|
||||
"Trick House Puzzle 5 - Reward",
|
||||
"Trick House Puzzle 6 - Reward",
|
||||
"Trick House Puzzle 7 - Reward",
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
VISITED_EVENT_NAME_TO_ID = {
|
||||
"EVENT_VISITED_LITTLEROOT_TOWN": 0,
|
||||
"EVENT_VISITED_OLDALE_TOWN": 1,
|
||||
@@ -80,7 +38,7 @@ class PokemonEmeraldLocation(Location):
|
||||
game: str = "Pokemon Emerald"
|
||||
item_address: Optional[int]
|
||||
default_item_code: Optional[int]
|
||||
tags: FrozenSet[str]
|
||||
key: Optional[str]
|
||||
|
||||
def __init__(
|
||||
self,
|
||||
@@ -88,13 +46,13 @@ class PokemonEmeraldLocation(Location):
|
||||
name: str,
|
||||
address: Optional[int],
|
||||
parent: Optional[Region] = None,
|
||||
key: Optional[str] = None,
|
||||
item_address: Optional[int] = None,
|
||||
default_item_value: Optional[int] = None,
|
||||
tags: FrozenSet[str] = frozenset()) -> None:
|
||||
default_item_value: Optional[int] = None) -> None:
|
||||
super().__init__(player, name, address, parent)
|
||||
self.default_item_code = None if default_item_value is None else offset_item_value(default_item_value)
|
||||
self.item_address = item_address
|
||||
self.tags = tags
|
||||
self.key = key
|
||||
|
||||
|
||||
def offset_flag(flag: int) -> int:
|
||||
@@ -115,16 +73,14 @@ def reverse_offset_flag(location_id: int) -> int:
|
||||
return location_id - BASE_OFFSET
|
||||
|
||||
|
||||
def create_locations_with_tags(world: "PokemonEmeraldWorld", regions: Dict[str, Region], tags: Iterable[str]) -> None:
|
||||
def create_locations_by_category(world: "PokemonEmeraldWorld", regions: Dict[str, Region], categories: Set[LocationCategory]) -> None:
|
||||
"""
|
||||
Iterates through region data and adds locations to the multiworld if
|
||||
those locations include any of the provided tags.
|
||||
"""
|
||||
tags = set(tags)
|
||||
|
||||
for region_name, region_data in data.regions.items():
|
||||
region = regions[region_name]
|
||||
filtered_locations = [loc for loc in region_data.locations if len(tags & data.locations[loc].tags) > 0]
|
||||
filtered_locations = [loc for loc in region_data.locations if data.locations[loc].category in categories]
|
||||
|
||||
for location_name in filtered_locations:
|
||||
location_data = data.locations[location_name]
|
||||
@@ -144,9 +100,9 @@ def create_locations_with_tags(world: "PokemonEmeraldWorld", regions: Dict[str,
|
||||
location_data.label,
|
||||
location_id,
|
||||
region,
|
||||
location_name,
|
||||
location_data.address,
|
||||
location_data.default_item,
|
||||
location_data.tags
|
||||
location_data.default_item
|
||||
)
|
||||
region.locations.append(location)
|
||||
|
||||
|
||||
@@ -6,7 +6,8 @@ from typing import TYPE_CHECKING, Callable, Dict
|
||||
from BaseClasses import CollectionState
|
||||
from worlds.generic.Rules import add_rule, set_rule
|
||||
|
||||
from .data import NATIONAL_ID_TO_SPECIES_ID, NUM_REAL_SPECIES, data
|
||||
from .data import LocationCategory, NATIONAL_ID_TO_SPECIES_ID, NUM_REAL_SPECIES, data
|
||||
from .locations import PokemonEmeraldLocation
|
||||
from .options import DarkCavesRequireFlash, EliteFourRequirement, NormanRequirement, Goal
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -23,7 +24,7 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
|
||||
state.has(hm, world.player) and state.has_all(badges, world.player)
|
||||
else:
|
||||
hm_rules[hm] = lambda state, hm=hm, badges=badges: \
|
||||
state.has(hm, world.player) and state.has_group_unique("Badges", world.player, badges)
|
||||
state.has(hm, world.player) and state.has_group_unique("Badge", world.player, badges)
|
||||
|
||||
def has_acro_bike(state: CollectionState):
|
||||
return state.has("Acro Bike", world.player)
|
||||
@@ -236,11 +237,11 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
|
||||
if world.options.norman_requirement == NormanRequirement.option_badges:
|
||||
set_rule(
|
||||
get_entrance("MAP_PETALBURG_CITY_GYM:2/MAP_PETALBURG_CITY_GYM:3"),
|
||||
lambda state: state.has_group_unique("Badges", world.player, world.options.norman_count.value)
|
||||
lambda state: state.has_group_unique("Badge", world.player, world.options.norman_count.value)
|
||||
)
|
||||
set_rule(
|
||||
get_entrance("MAP_PETALBURG_CITY_GYM:5/MAP_PETALBURG_CITY_GYM:6"),
|
||||
lambda state: state.has_group_unique("Badges", world.player, world.options.norman_count.value)
|
||||
lambda state: state.has_group_unique("Badge", world.player, world.options.norman_count.value)
|
||||
)
|
||||
else:
|
||||
set_rule(
|
||||
@@ -1506,7 +1507,7 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
|
||||
if world.options.elite_four_requirement == EliteFourRequirement.option_badges:
|
||||
set_rule(
|
||||
get_entrance("REGION_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F/MAIN -> REGION_EVER_GRANDE_CITY_POKEMON_LEAGUE_1F/BEHIND_BADGE_CHECKERS"),
|
||||
lambda state: state.has_group_unique("Badges", world.player, world.options.elite_four_count.value)
|
||||
lambda state: state.has_group_unique("Badge", world.player, world.options.elite_four_count.value)
|
||||
)
|
||||
else:
|
||||
set_rule(
|
||||
@@ -1659,7 +1660,8 @@ def set_rules(world: "PokemonEmeraldWorld") -> None:
|
||||
# Add Itemfinder requirement to hidden items
|
||||
if world.options.require_itemfinder:
|
||||
for location in world.multiworld.get_locations(world.player):
|
||||
if location.tags is not None and "HiddenItem" in location.tags:
|
||||
assert isinstance(location, PokemonEmeraldLocation)
|
||||
if location.key is not None and data.locations[location.key].category == LocationCategory.HIDDEN_ITEM:
|
||||
add_rule(
|
||||
location,
|
||||
lambda state: state.has("Itemfinder", world.player)
|
||||
|
||||
@@ -5,8 +5,6 @@ duplicate claims and give warnings for unused and unignored locations or warps.
|
||||
import logging
|
||||
from typing import List
|
||||
|
||||
from .data import load_json_data, data
|
||||
|
||||
|
||||
_IGNORABLE_LOCATIONS = frozenset({
|
||||
"HIDDEN_ITEM_TRICK_HOUSE_NUGGET", # Is permanently mssiable and has special behavior that sets the flag early
|
||||
@@ -247,12 +245,29 @@ _IGNORABLE_WARPS = frozenset({
|
||||
})
|
||||
|
||||
|
||||
def validate_group_maps() -> bool:
|
||||
from .data import data
|
||||
from .groups import _LOCATION_GROUP_MAPS
|
||||
|
||||
failed = False
|
||||
|
||||
for group_name, map_set in _LOCATION_GROUP_MAPS.items():
|
||||
for map_name in map_set:
|
||||
if map_name not in data.maps:
|
||||
failed = True
|
||||
logging.error("Pokemon Emerald: Map named %s in location group %s does not exist", map_name, group_name)
|
||||
|
||||
return not failed
|
||||
|
||||
|
||||
def validate_regions() -> bool:
|
||||
"""
|
||||
Verifies that Emerald's data doesn't have duplicate or missing
|
||||
regions/warps/locations. Meant to catch problems during development like
|
||||
forgetting to add a new location or incorrectly splitting a region.
|
||||
"""
|
||||
from .data import load_json_data, data
|
||||
|
||||
extracted_data_json = load_json_data("extracted_data.json")
|
||||
error_messages: List[str] = []
|
||||
warn_messages: List[str] = []
|
||||
|
||||
@@ -11,8 +11,7 @@ Al usar BizHawk, esta guía solo es aplicable en los sistemas de Windows y Linux
|
||||
- Instrucciones de instalación detalladas para BizHawk se pueden encontrar en el enlace de arriba.
|
||||
- Los usuarios de Windows deben ejecutar el instalador de prerrequisitos (prereq installer) primero, que también se
|
||||
encuentra en el enlace de arriba.
|
||||
- El cliente incorporado de Archipelago, que se puede encontrar [aquí](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
(selecciona `Pokemon Client` durante la instalación).
|
||||
- El cliente incorporado de Archipelago, que se puede encontrar [aquí](https://github.com/ArchipelagoMW/Archipelago/releases).
|
||||
- Los ROMs originales de Pokémon Red y/o Blue. La comunidad de Archipelago no puede proveerlos.
|
||||
|
||||
## Software Opcional
|
||||
@@ -75,27 +74,41 @@ Y los siguientes caracteres especiales (cada uno ocupa un carácter):
|
||||
|
||||
## Unirse a un juego MultiWorld
|
||||
|
||||
### Obtener tu parche de Pokémon
|
||||
### Generar y parchar un juego
|
||||
|
||||
Cuando te unes a un juego multiworld, se te pedirá que entregues tu archivo YAML a quien lo esté organizando.
|
||||
Una vez que la generación acabe, el anfitrión te dará un enlace a tu archivo, o un .zip con los archivos de
|
||||
todos. Tu archivo tiene una extensión `.apred` o `.apblue`.
|
||||
1. Crea tu archivo de opciones (YAML).
|
||||
2. Sigue las instrucciones generales de Archipelago para [generar un juego](../../Archipelago/setup/en#generating-a-game).
|
||||
Haciendo esto se generará un archivo de salida. Tu parche tendrá la extensión de archivo `.apred` o `.apblue`.
|
||||
3. Abre `ArchipelagoLauncher.exe`
|
||||
4. Selecciona "Open Patch" en el lado izquierdo y selecciona tu parche.
|
||||
5. Si es tu primera vez parchando, se te pedirá que selecciones tu ROM original.
|
||||
6. Un archivo `.gb` parchado será creado en el mismo lugar donde está el parche.
|
||||
7. La primera vez que abras un parche con BizHawk Client, también se te pedira ubicar `EmuHawk.exe` en tu
|
||||
instalación de BizHawk.
|
||||
|
||||
Haz doble clic en tu archivo `.apred` o `.apblue` para que se ejecute el cliente y realice el parcheado del ROM.
|
||||
Una vez acabe ese proceso (esto puede tardar un poco), el cliente y el emulador se abrirán automáticamente (si es que se
|
||||
ha asociado la extensión al emulador tal como fue recomendado)
|
||||
Si estás jugando una semilla single-player y no te importa tener seguimiento ni pistas, puedes terminar aqui, cerrar el
|
||||
cliente, y cargar el ROM parchado en cualquier emulador. Sin embargo, para multiworlds y otras funciones de Archipelago,
|
||||
continúa con los pasos abajo, usando el emulador BizHawk.
|
||||
|
||||
### Conectarse al multiserver
|
||||
|
||||
Una vez ejecutado tanto el cliente como el emulador, hay que conectarlos. Abre la carpeta de instalación de Archipelago,
|
||||
luego abre `data/lua`, y simplemente arrastra el archivo `connector_pkmn_rb.lua` a la ventana principal de Emuhawk.
|
||||
(Alternativamente, puedes abrir la consola de Lua manualmente. En Emuhawk ir a Tools > Lua Console, luego ir al menú
|
||||
`Script` 〉 `Open Script`, navegar a la ubicación de `connector_pkmn_rb.lua` y seleccionarlo.)
|
||||
Por defecto, abrir un parche hará los pasos del 1 al 5 automáticamente. Incluso asi, es bueno memorizarlos en caso de
|
||||
que tengas que cerrar y volver a abrir el juego por alguna razón.
|
||||
|
||||
Para conectar el cliente con el servidor, simplemente pon `<dirección>:<puerto>` en la caja de texto superior y presiona
|
||||
enter (si el servidor tiene contraseña, en la caja de texto inferior escribir `/connect <dirección>:<puerto> [contraseña]`)
|
||||
1. Pokémon Red/Blue usa el BizHawk Client de Archipelago. Si el cliente no está abierto desde cuando parchaste tu juego,
|
||||
puedes volverlo a abrir desde el Launcher.
|
||||
2. Asegúrate que EmuHawk esta cargando el ROM parchado.
|
||||
3. En EmuHawk, ir a `Tools > Lua Console`. Esta ventana debe quedarse abierta mientras se juega.
|
||||
4. En la ventana de Lua Console, ir a `Script > Open Script…`.
|
||||
5. Navegar a tu carpeta de instalación de Archipelago y abrir `data/lua/connector_bizhawk_generic.lua`.
|
||||
6. El emulador se puede congelar por unos segundos hasta que logre conectarse al cliente. Esto es normal. La ventana del
|
||||
BizHawk Client debería indicar que se logro conectar y reconocer Pokémon Red/Blue.
|
||||
7. Para conectar el cliente al servidor, ingresa la dirección y el puerto (por ejemplo, `archipelago.gg:38281`) en el
|
||||
campo de texto superior del cliente y y haz clic en Connect.
|
||||
|
||||
Ahora ya estás listo para tu aventura en Kanto.
|
||||
Para conectar el cliente al multiserver simplemente escribe `<dirección>:<puerto>` en el campo de texto superior y
|
||||
presiona enter (si el servidor usa contraseña, escribe en el campo de texto inferior
|
||||
`/connect <dirección>:<puerto>[contraseña]`)
|
||||
|
||||
## Auto-Tracking
|
||||
|
||||
|
||||
@@ -223,7 +223,7 @@ location_data = [
|
||||
Missable(92)),
|
||||
LocationData("Victory Road 2F-C", "East Item", "Full Heal", rom_addresses["Missable_Victory_Road_2F_Item_2"],
|
||||
Missable(93)),
|
||||
LocationData("Victory Road 2F-W", "West Item", "TM05 Mega Kick", rom_addresses["Missable_Victory_Road_2F_Item_3"],
|
||||
LocationData("Victory Road 2F-C", "West Item", "TM05 Mega Kick", rom_addresses["Missable_Victory_Road_2F_Item_3"],
|
||||
Missable(94)),
|
||||
LocationData("Victory Road 2F-NW", "North Item Near Moltres", "Guard Spec", rom_addresses["Missable_Victory_Road_2F_Item_4"],
|
||||
Missable(95)),
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
import typing
|
||||
from dataclasses import dataclass
|
||||
from Options import DefaultOnToggle, Range, Toggle, DeathLink, Choice, PerGameCommonOptions, OptionSet
|
||||
from Options import DefaultOnToggle, Range, Toggle, DeathLink, Choice, PerGameCommonOptions, OptionSet, OptionGroup
|
||||
from .Items import action_item_table
|
||||
|
||||
class EnableCoinStars(DefaultOnToggle):
|
||||
@@ -127,6 +127,32 @@ class MoveRandomizerActions(OptionSet):
|
||||
valid_keys = [action for action in action_item_table if action != 'Double Jump']
|
||||
default = valid_keys
|
||||
|
||||
sm64_options_groups = [
|
||||
OptionGroup("Logic Options", [
|
||||
AreaRandomizer,
|
||||
BuddyChecks,
|
||||
ExclamationBoxes,
|
||||
ProgressiveKeys,
|
||||
EnableCoinStars,
|
||||
StrictCapRequirements,
|
||||
StrictCannonRequirements,
|
||||
]),
|
||||
OptionGroup("Ability Options", [
|
||||
EnableMoveRandomizer,
|
||||
MoveRandomizerActions,
|
||||
StrictMoveRequirements,
|
||||
]),
|
||||
OptionGroup("Star Options", [
|
||||
AmountOfStars,
|
||||
FirstBowserStarDoorCost,
|
||||
BasementStarDoorCost,
|
||||
SecondFloorStarDoorCost,
|
||||
MIPS1Cost,
|
||||
MIPS2Cost,
|
||||
StarsToFinish,
|
||||
]),
|
||||
]
|
||||
|
||||
@dataclass
|
||||
class SM64Options(PerGameCommonOptions):
|
||||
area_rando: AreaRandomizer
|
||||
|
||||
@@ -3,7 +3,7 @@ import os
|
||||
import json
|
||||
from .Items import item_table, action_item_table, cannon_item_table, SM64Item
|
||||
from .Locations import location_table, SM64Location
|
||||
from .Options import SM64Options
|
||||
from .Options import sm64_options_groups, SM64Options
|
||||
from .Rules import set_rules
|
||||
from .Regions import create_regions, sm64_level_to_entrances, SM64Levels
|
||||
from BaseClasses import Item, Tutorial, ItemClassification, Region
|
||||
@@ -20,6 +20,8 @@ class SM64Web(WebWorld):
|
||||
["N00byKing"]
|
||||
)]
|
||||
|
||||
option_groups = sm64_options_groups
|
||||
|
||||
|
||||
class SM64World(World):
|
||||
"""
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
import typing
|
||||
from Options import Choice, Option, PerGameCommonOptions, Toggle, DefaultOnToggle, Range, ItemsAccessibility
|
||||
|
||||
from Options import Choice, Option, PerGameCommonOptions, Toggle, DefaultOnToggle, Range, ItemsAccessibility, StartInventoryPool
|
||||
from dataclasses import dataclass
|
||||
|
||||
class SMLogic(Choice):
|
||||
@@ -129,6 +130,7 @@ class EnergyBeep(DefaultOnToggle):
|
||||
|
||||
@dataclass
|
||||
class SMZ3Options(PerGameCommonOptions):
|
||||
start_inventory_from_pool: StartInventoryPool
|
||||
accessibility: ItemsAccessibility
|
||||
sm_logic: SMLogic
|
||||
sword_location: SwordLocation
|
||||
|
||||
@@ -206,25 +206,10 @@ class StardewValleyWorld(World):
|
||||
self.multiworld.push_precollected(self.create_starting_item("Progressive Coop"))
|
||||
|
||||
def setup_player_events(self):
|
||||
self.setup_construction_events()
|
||||
self.setup_quest_events()
|
||||
self.setup_action_events()
|
||||
self.setup_logic_events()
|
||||
|
||||
def setup_construction_events(self):
|
||||
can_construct_buildings = LocationData(None, RegionName.carpenter, Event.can_construct_buildings)
|
||||
self.create_event_location(can_construct_buildings, True_(), Event.can_construct_buildings)
|
||||
|
||||
def setup_quest_events(self):
|
||||
start_dark_talisman_quest = LocationData(None, RegionName.railroad, Event.start_dark_talisman_quest)
|
||||
self.create_event_location(start_dark_talisman_quest, self.logic.wallet.has_rusty_key(), Event.start_dark_talisman_quest)
|
||||
|
||||
def setup_action_events(self):
|
||||
can_ship_event = LocationData(None, LogicRegion.shipping, Event.can_ship_items)
|
||||
self.create_event_location(can_ship_event, true_, Event.can_ship_items)
|
||||
can_shop_pierre_event = LocationData(None, RegionName.pierre_store, Event.can_shop_at_pierre)
|
||||
self.create_event_location(can_shop_pierre_event, true_, Event.can_shop_at_pierre)
|
||||
|
||||
spring_farming = LocationData(None, LogicRegion.spring_farming, Event.spring_farming)
|
||||
self.create_event_location(spring_farming, true_, Event.spring_farming)
|
||||
summer_farming = LocationData(None, LogicRegion.summer_farming, Event.summer_farming)
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from functools import cached_property
|
||||
from typing import Dict, Union
|
||||
|
||||
from Utils import cache_self1
|
||||
@@ -8,12 +9,12 @@ from .received_logic import ReceivedLogicMixin
|
||||
from .region_logic import RegionLogicMixin
|
||||
from ..options import BuildingProgression
|
||||
from ..stardew_rule import StardewRule, True_, False_, Has
|
||||
from ..strings.ap_names.event_names import Event
|
||||
from ..strings.artisan_good_names import ArtisanGood
|
||||
from ..strings.building_names import Building
|
||||
from ..strings.fish_names import WaterItem
|
||||
from ..strings.material_names import Material
|
||||
from ..strings.metal_names import MetalBar
|
||||
from ..strings.region_names import Region
|
||||
|
||||
has_group = "building"
|
||||
|
||||
@@ -60,7 +61,7 @@ class BuildingLogic(BaseLogic[Union[BuildingLogicMixin, MoneyLogicMixin, RegionL
|
||||
return True_()
|
||||
return self.logic.received(building)
|
||||
|
||||
carpenter_rule = self.logic.received(Event.can_construct_buildings)
|
||||
carpenter_rule = self.logic.building.can_construct_buildings
|
||||
if not self.options.building_progression & BuildingProgression.option_progressive:
|
||||
return Has(building, self.registry.building_rules, has_group) & carpenter_rule
|
||||
|
||||
@@ -75,6 +76,10 @@ class BuildingLogic(BaseLogic[Union[BuildingLogicMixin, MoneyLogicMixin, RegionL
|
||||
building = " ".join(["Progressive", *building.split(" ")[1:]])
|
||||
return self.logic.received(building, count) & carpenter_rule
|
||||
|
||||
@cached_property
|
||||
def can_construct_buildings(self) -> StardewRule:
|
||||
return self.logic.region.can_reach(Region.carpenter)
|
||||
|
||||
@cache_self1
|
||||
def has_house(self, upgrade_level: int) -> StardewRule:
|
||||
if upgrade_level < 1:
|
||||
@@ -83,7 +88,7 @@ class BuildingLogic(BaseLogic[Union[BuildingLogicMixin, MoneyLogicMixin, RegionL
|
||||
if upgrade_level > 3:
|
||||
return False_()
|
||||
|
||||
carpenter_rule = self.logic.received(Event.can_construct_buildings)
|
||||
carpenter_rule = self.logic.building.can_construct_buildings
|
||||
if self.options.building_progression & BuildingProgression.option_progressive:
|
||||
return carpenter_rule & self.logic.received(f"Progressive House", upgrade_level)
|
||||
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
import typing
|
||||
from typing import Union
|
||||
|
||||
from Utils import cache_self1
|
||||
@@ -11,10 +12,14 @@ from .time_logic import TimeLogicMixin
|
||||
from ..data.shop import ShopSource
|
||||
from ..options import SpecialOrderLocations
|
||||
from ..stardew_rule import StardewRule, True_, HasProgressionPercent, False_, true_
|
||||
from ..strings.ap_names.event_names import Event
|
||||
from ..strings.currency_names import Currency
|
||||
from ..strings.region_names import Region, LogicRegion
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
from .shipping_logic import ShippingLogicMixin
|
||||
|
||||
assert ShippingLogicMixin
|
||||
|
||||
qi_gem_rewards = ("100 Qi Gems", "50 Qi Gems", "40 Qi Gems", "35 Qi Gems", "25 Qi Gems",
|
||||
"20 Qi Gems", "15 Qi Gems", "10 Qi Gems")
|
||||
|
||||
@@ -26,7 +31,7 @@ class MoneyLogicMixin(BaseLogicMixin):
|
||||
|
||||
|
||||
class MoneyLogic(BaseLogic[Union[RegionLogicMixin, MoneyLogicMixin, TimeLogicMixin, RegionLogicMixin, ReceivedLogicMixin, HasLogicMixin, SeasonLogicMixin,
|
||||
GrindLogicMixin]]):
|
||||
GrindLogicMixin, 'ShippingLogicMixin']]):
|
||||
|
||||
@cache_self1
|
||||
def can_have_earned_total(self, amount: int) -> StardewRule:
|
||||
@@ -37,7 +42,7 @@ GrindLogicMixin]]):
|
||||
willy_rule = self.logic.region.can_reach_all((Region.fish_shop, LogicRegion.fishing))
|
||||
clint_rule = self.logic.region.can_reach_all((Region.blacksmith, Region.mines_floor_5))
|
||||
robin_rule = self.logic.region.can_reach_all((Region.carpenter, Region.secret_woods))
|
||||
shipping_rule = self.logic.received(Event.can_ship_items)
|
||||
shipping_rule = self.logic.shipping.can_use_shipping_bin
|
||||
|
||||
if amount < 2000:
|
||||
selling_any_rule = pierre_rule | willy_rule | clint_rule | robin_rule | shipping_rule
|
||||
@@ -50,7 +55,7 @@ GrindLogicMixin]]):
|
||||
if amount < 10000:
|
||||
return shipping_rule
|
||||
|
||||
seed_rules = self.logic.received(Event.can_shop_at_pierre)
|
||||
seed_rules = self.logic.region.can_reach(Region.pierre_store)
|
||||
if amount < 40000:
|
||||
return shipping_rule & seed_rules
|
||||
|
||||
|
||||
@@ -11,7 +11,6 @@ from ..locations import LocationTags, locations_by_tag
|
||||
from ..options import ExcludeGingerIsland, Shipsanity
|
||||
from ..options import SpecialOrderLocations
|
||||
from ..stardew_rule import StardewRule
|
||||
from ..strings.ap_names.event_names import Event
|
||||
from ..strings.building_names import Building
|
||||
|
||||
|
||||
@@ -29,7 +28,7 @@ class ShippingLogic(BaseLogic[Union[ReceivedLogicMixin, ShippingLogicMixin, Buil
|
||||
|
||||
@cache_self1
|
||||
def can_ship(self, item: str) -> StardewRule:
|
||||
return self.logic.received(Event.can_ship_items) & self.logic.has(item)
|
||||
return self.logic.shipping.can_use_shipping_bin & self.logic.has(item)
|
||||
|
||||
def can_ship_everything(self) -> StardewRule:
|
||||
shipsanity_prefix = "Shipsanity: "
|
||||
@@ -49,7 +48,7 @@ class ShippingLogic(BaseLogic[Union[ReceivedLogicMixin, ShippingLogicMixin, Buil
|
||||
|
||||
def can_ship_everything_in_slot(self, all_location_names_in_slot: List[str]) -> StardewRule:
|
||||
if self.options.shipsanity == Shipsanity.option_none:
|
||||
return self.can_ship_everything()
|
||||
return self.logic.shipping.can_ship_everything()
|
||||
|
||||
rules = [self.logic.building.has_building(Building.shipping_bin)]
|
||||
|
||||
|
||||
@@ -21,7 +21,6 @@ from ..content.vanilla.ginger_island import ginger_island_content_pack
|
||||
from ..content.vanilla.qi_board import qi_board_content_pack
|
||||
from ..stardew_rule import StardewRule, Has, false_
|
||||
from ..strings.animal_product_names import AnimalProduct
|
||||
from ..strings.ap_names.event_names import Event
|
||||
from ..strings.ap_names.transport_names import Transportation
|
||||
from ..strings.artisan_good_names import ArtisanGood
|
||||
from ..strings.crop_names import Vegetable, Fruit
|
||||
@@ -61,7 +60,7 @@ AbilityLogicMixin, SpecialOrderLogicMixin, MonsterLogicMixin]]):
|
||||
SpecialOrder.gifts_for_george: self.logic.season.has(Season.spring) & self.logic.has(Forageable.leek),
|
||||
SpecialOrder.fragments_of_the_past: self.logic.monster.can_kill(Monster.skeleton),
|
||||
SpecialOrder.gus_famous_omelet: self.logic.has(AnimalProduct.any_egg),
|
||||
SpecialOrder.crop_order: self.logic.ability.can_farm_perfectly() & self.logic.received(Event.can_ship_items),
|
||||
SpecialOrder.crop_order: self.logic.ability.can_farm_perfectly() & self.logic.shipping.can_use_shipping_bin,
|
||||
SpecialOrder.community_cleanup: self.logic.skill.can_crab_pot,
|
||||
SpecialOrder.the_strong_stuff: self.logic.has(ArtisanGood.specific_juice(Vegetable.potato)),
|
||||
SpecialOrder.pierres_prime_produce: self.logic.ability.can_farm_perfectly(),
|
||||
@@ -94,12 +93,12 @@ AbilityLogicMixin, SpecialOrderLogicMixin, MonsterLogicMixin]]):
|
||||
self.update_rules({
|
||||
SpecialOrder.qis_crop: self.logic.ability.can_farm_perfectly() & self.logic.region.can_reach(Region.greenhouse) &
|
||||
self.logic.region.can_reach(Region.island_west) & self.logic.skill.has_total_level(50) &
|
||||
self.logic.has(Machine.seed_maker) & self.logic.received(Event.can_ship_items),
|
||||
self.logic.has(Machine.seed_maker) & self.logic.shipping.can_use_shipping_bin,
|
||||
SpecialOrder.lets_play_a_game: self.logic.arcade.has_junimo_kart_max_level(),
|
||||
SpecialOrder.four_precious_stones: self.logic.time.has_lived_max_months & self.logic.has("Prismatic Shard") &
|
||||
self.logic.ability.can_mine_perfectly_in_the_skull_cavern(),
|
||||
SpecialOrder.qis_hungry_challenge: self.logic.ability.can_mine_perfectly_in_the_skull_cavern(),
|
||||
SpecialOrder.qis_cuisine: self.logic.cooking.can_cook() & self.logic.received(Event.can_ship_items) &
|
||||
SpecialOrder.qis_cuisine: self.logic.cooking.can_cook() & self.logic.shipping.can_use_shipping_bin &
|
||||
(self.logic.money.can_spend_at(Region.saloon, 205000) | self.logic.money.can_spend_at(Region.pierre_store, 170000)),
|
||||
SpecialOrder.qis_kindness: self.logic.relationship.can_give_loved_gifts_to_everyone(),
|
||||
SpecialOrder.extended_family: self.logic.ability.can_fish_perfectly() & self.logic.has(Fish.angler) & self.logic.has(Fish.glacierfish) &
|
||||
|
||||
@@ -27,7 +27,6 @@ from .stardew_rule.indirect_connection import look_for_indirect_connection
|
||||
from .stardew_rule.rule_explain import explain
|
||||
from .strings.ap_names.ap_option_names import OptionName
|
||||
from .strings.ap_names.community_upgrade_names import CommunityUpgrade
|
||||
from .strings.ap_names.event_names import Event
|
||||
from .strings.ap_names.mods.mod_items import SVEQuestItem, SVERunes
|
||||
from .strings.ap_names.transport_names import Transportation
|
||||
from .strings.artisan_good_names import ArtisanGood
|
||||
@@ -251,7 +250,8 @@ def set_entrance_rules(logic: StardewLogic, multiworld, player, world_options: S
|
||||
set_entrance_rule(multiworld, player, Entrance.enter_witch_warp_cave, logic.quest.has_dark_talisman() | (logic.mod.magic.can_blink()))
|
||||
set_entrance_rule(multiworld, player, Entrance.enter_witch_hut, (logic.has(ArtisanGood.void_mayonnaise) | logic.mod.magic.can_blink()))
|
||||
set_entrance_rule(multiworld, player, Entrance.enter_mutant_bug_lair,
|
||||
(logic.received(Event.start_dark_talisman_quest) & logic.relationship.can_meet(NPC.krobus)) | logic.mod.magic.can_blink())
|
||||
(logic.wallet.has_rusty_key() & logic.region.can_reach(Region.railroad) & logic.relationship.can_meet(
|
||||
NPC.krobus)) | logic.mod.magic.can_blink())
|
||||
set_entrance_rule(multiworld, player, Entrance.enter_casino, logic.quest.has_club_card())
|
||||
|
||||
set_bedroom_entrance_rules(logic, multiworld, player, world_options)
|
||||
@@ -307,8 +307,7 @@ def set_mines_floor_entrance_rules(logic, multiworld, player):
|
||||
rule = logic.mine.has_mine_elevator_to_floor(floor - 10)
|
||||
if floor == 5 or floor == 45 or floor == 85:
|
||||
rule = rule & logic.mine.can_progress_in_the_mines_from_floor(floor)
|
||||
entrance = multiworld.get_entrance(dig_to_mines_floor(floor), player)
|
||||
MultiWorldRules.set_rule(entrance, rule)
|
||||
set_entrance_rule(multiworld, player, dig_to_mines_floor(floor), rule)
|
||||
|
||||
|
||||
def set_skull_cavern_floor_entrance_rules(logic, multiworld, player):
|
||||
@@ -316,8 +315,7 @@ def set_skull_cavern_floor_entrance_rules(logic, multiworld, player):
|
||||
rule = logic.mod.elevator.has_skull_cavern_elevator_to_floor(floor - 25)
|
||||
if floor == 25 or floor == 75 or floor == 125:
|
||||
rule = rule & logic.mine.can_progress_in_the_skull_cavern_from_floor(floor)
|
||||
entrance = multiworld.get_entrance(dig_to_skull_floor(floor), player)
|
||||
MultiWorldRules.set_rule(entrance, rule)
|
||||
set_entrance_rule(multiworld, player, dig_to_skull_floor(floor), rule)
|
||||
|
||||
|
||||
def set_blacksmith_entrance_rules(logic, multiworld, player):
|
||||
@@ -346,9 +344,8 @@ def set_skill_entrance_rules(logic, multiworld, player, world_options: StardewVa
|
||||
|
||||
|
||||
def set_blacksmith_upgrade_rule(logic, multiworld, player, entrance_name: str, item_name: str, tool_material: str):
|
||||
material_entrance = multiworld.get_entrance(entrance_name, player)
|
||||
upgrade_rule = logic.has(item_name) & logic.money.can_spend(tool_upgrade_prices[tool_material])
|
||||
MultiWorldRules.set_rule(material_entrance, upgrade_rule)
|
||||
set_entrance_rule(multiworld, player, entrance_name, upgrade_rule)
|
||||
|
||||
|
||||
def set_festival_entrance_rules(logic, multiworld, player):
|
||||
@@ -880,25 +877,19 @@ def set_traveling_merchant_day_rules(logic: StardewLogic, multiworld: MultiWorld
|
||||
|
||||
|
||||
def set_arcade_machine_rules(logic: StardewLogic, multiworld: MultiWorld, player: int, world_options: StardewValleyOptions):
|
||||
MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.play_junimo_kart, player),
|
||||
logic.received(Wallet.skull_key))
|
||||
play_junimo_kart_rule = logic.received(Wallet.skull_key)
|
||||
|
||||
if world_options.arcade_machine_locations != ArcadeMachineLocations.option_full_shuffling:
|
||||
set_entrance_rule(multiworld, player, Entrance.play_junimo_kart, play_junimo_kart_rule)
|
||||
return
|
||||
|
||||
MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.play_junimo_kart, player),
|
||||
logic.has("Junimo Kart Small Buff"))
|
||||
MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.reach_junimo_kart_2, player),
|
||||
logic.has("Junimo Kart Medium Buff"))
|
||||
MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.reach_junimo_kart_3, player),
|
||||
logic.has("Junimo Kart Big Buff"))
|
||||
MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.reach_junimo_kart_4, player),
|
||||
logic.has("Junimo Kart Max Buff"))
|
||||
MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.play_journey_of_the_prairie_king, player),
|
||||
logic.has("JotPK Small Buff"))
|
||||
MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.reach_jotpk_world_2, player),
|
||||
logic.has("JotPK Medium Buff"))
|
||||
MultiWorldRules.add_rule(multiworld.get_entrance(Entrance.reach_jotpk_world_3, player),
|
||||
logic.has("JotPK Big Buff"))
|
||||
set_entrance_rule(multiworld, player, Entrance.play_junimo_kart, play_junimo_kart_rule & logic.has("Junimo Kart Small Buff"))
|
||||
set_entrance_rule(multiworld, player, Entrance.reach_junimo_kart_2, logic.has("Junimo Kart Medium Buff"))
|
||||
set_entrance_rule(multiworld, player, Entrance.reach_junimo_kart_3, logic.has("Junimo Kart Big Buff"))
|
||||
set_entrance_rule(multiworld, player, Entrance.reach_junimo_kart_4, logic.has("Junimo Kart Max Buff"))
|
||||
set_entrance_rule(multiworld, player, Entrance.play_journey_of_the_prairie_king, logic.has("JotPK Small Buff"))
|
||||
set_entrance_rule(multiworld, player, Entrance.reach_jotpk_world_2, logic.has("JotPK Medium Buff"))
|
||||
set_entrance_rule(multiworld, player, Entrance.reach_jotpk_world_3, logic.has("JotPK Big Buff"))
|
||||
MultiWorldRules.add_rule(multiworld.get_location("Journey of the Prairie King Victory", player),
|
||||
logic.has("JotPK Max Buff"))
|
||||
|
||||
@@ -1049,6 +1040,7 @@ def set_entrance_rule(multiworld, player, entrance: str, rule: StardewRule):
|
||||
potentially_required_regions = look_for_indirect_connection(rule)
|
||||
if potentially_required_regions:
|
||||
for region in potentially_required_regions:
|
||||
logger.debug(f"Registering indirect condition for {region} -> {entrance}")
|
||||
multiworld.register_indirect_condition(multiworld.get_region(region, player), multiworld.get_entrance(entrance, player))
|
||||
|
||||
MultiWorldRules.set_rule(multiworld.get_entrance(entrance, player), rule)
|
||||
|
||||
@@ -4,7 +4,7 @@ from dataclasses import dataclass, field
|
||||
from functools import cached_property, singledispatch
|
||||
from typing import Iterable, Set, Tuple, List, Optional
|
||||
|
||||
from BaseClasses import CollectionState
|
||||
from BaseClasses import CollectionState, Location, Entrance
|
||||
from worlds.generic.Rules import CollectionRule
|
||||
from . import StardewRule, AggregatingStardewRule, Count, Has, TotalReceived, Received, Reach, true_
|
||||
|
||||
@@ -12,10 +12,10 @@ from . import StardewRule, AggregatingStardewRule, Count, Has, TotalReceived, Re
|
||||
@dataclass
|
||||
class RuleExplanation:
|
||||
rule: StardewRule
|
||||
state: CollectionState
|
||||
state: CollectionState = field(repr=False, hash=False)
|
||||
expected: bool
|
||||
sub_rules: Iterable[StardewRule] = field(default_factory=list)
|
||||
explored_rules_key: Set[Tuple[str, str]] = field(default_factory=set)
|
||||
explored_rules_key: Set[Tuple[str, str]] = field(default_factory=set, repr=False, hash=False)
|
||||
current_rule_explored: bool = False
|
||||
|
||||
def __post_init__(self):
|
||||
@@ -38,13 +38,6 @@ class RuleExplanation:
|
||||
if i.result is not self.expected else i.summary(depth + 1)
|
||||
for i in sorted(self.explained_sub_rules, key=lambda x: x.result))
|
||||
|
||||
def __repr__(self, depth=0):
|
||||
if not self.sub_rules:
|
||||
return self.summary(depth)
|
||||
|
||||
return self.summary(depth) + "\n" + "\n".join(i.__repr__(depth + 1)
|
||||
for i in sorted(self.explained_sub_rules, key=lambda x: x.result))
|
||||
|
||||
@cached_property
|
||||
def result(self) -> bool:
|
||||
try:
|
||||
@@ -134,6 +127,10 @@ def _(rule: Reach, state: CollectionState, expected: bool, explored_spots: Set[T
|
||||
access_rules = [Reach(spot.parent_region.name, "Region", rule.player)]
|
||||
else:
|
||||
access_rules = [spot.access_rule, Reach(spot.parent_region.name, "Region", rule.player)]
|
||||
elif spot.access_rule == Location.access_rule:
|
||||
# Sometime locations just don't have an access rule and all the relevant logic is in the parent region.
|
||||
access_rules = [Reach(spot.parent_region.name, "Region", rule.player)]
|
||||
|
||||
|
||||
elif rule.resolution_hint == 'Entrance':
|
||||
spot = state.multiworld.get_entrance(rule.spot, rule.player)
|
||||
@@ -143,6 +140,9 @@ def _(rule: Reach, state: CollectionState, expected: bool, explored_spots: Set[T
|
||||
access_rules = [Reach(spot.parent_region.name, "Region", rule.player)]
|
||||
else:
|
||||
access_rules = [spot.access_rule, Reach(spot.parent_region.name, "Region", rule.player)]
|
||||
elif spot.access_rule == Entrance.access_rule:
|
||||
# Sometime entrances just don't have an access rule and all the relevant logic is in the parent region.
|
||||
access_rules = [Reach(spot.parent_region.name, "Region", rule.player)]
|
||||
|
||||
else:
|
||||
spot = state.multiworld.get_region(rule.spot, rule.player)
|
||||
|
||||
@@ -8,10 +8,6 @@ def event(name: str):
|
||||
|
||||
class Event:
|
||||
victory = event("Victory")
|
||||
can_construct_buildings = event("Can Construct Buildings")
|
||||
start_dark_talisman_quest = event("Start Dark Talisman Quest")
|
||||
can_ship_items = event("Can Ship Items")
|
||||
can_shop_at_pierre = event("Can Shop At Pierre's")
|
||||
spring_farming = event("Spring Farming")
|
||||
summer_farming = event("Summer Farming")
|
||||
fall_farming = event("Fall Farming")
|
||||
|
||||
@@ -23,10 +23,6 @@ class TestBuildingLogic(SVTestBase):
|
||||
self.assertFalse(big_coop_blueprint_rule(self.multiworld.state),
|
||||
f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}")
|
||||
|
||||
self.multiworld.state.collect(self.create_item("Can Construct Buildings"), prevent_sweep=True)
|
||||
self.assertFalse(big_coop_blueprint_rule(self.multiworld.state),
|
||||
f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}")
|
||||
|
||||
self.multiworld.state.collect(self.create_item("Progressive Coop"), prevent_sweep=False)
|
||||
self.assertTrue(big_coop_blueprint_rule(self.multiworld.state),
|
||||
f"Rule is {repr(self.multiworld.get_location('Big Coop Blueprint', self.player).access_rule)}")
|
||||
@@ -35,7 +31,6 @@ class TestBuildingLogic(SVTestBase):
|
||||
self.assertFalse(self.world.logic.region.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state))
|
||||
|
||||
self.collect_lots_of_money()
|
||||
self.multiworld.state.collect(self.create_item("Can Construct Buildings"), prevent_sweep=True)
|
||||
self.assertFalse(self.world.logic.region.can_reach_location("Deluxe Coop Blueprint")(self.multiworld.state))
|
||||
|
||||
self.multiworld.state.collect(self.create_item("Progressive Coop"), prevent_sweep=True)
|
||||
@@ -53,10 +48,6 @@ class TestBuildingLogic(SVTestBase):
|
||||
self.assertFalse(big_shed_rule(self.multiworld.state),
|
||||
f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}")
|
||||
|
||||
self.multiworld.state.collect(self.create_item("Can Construct Buildings"), prevent_sweep=True)
|
||||
self.assertFalse(big_shed_rule(self.multiworld.state),
|
||||
f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}")
|
||||
|
||||
self.multiworld.state.collect(self.create_item("Progressive Shed"), prevent_sweep=True)
|
||||
self.assertTrue(big_shed_rule(self.multiworld.state),
|
||||
f"Rule is {repr(self.multiworld.get_location('Big Shed Blueprint', self.player).access_rule)}")
|
||||
|
||||
@@ -138,7 +138,7 @@ item_table: Dict[str, ItemData] = {
|
||||
'Elevator Keycard': ItemData('Relic', 1337125, progression=True),
|
||||
'Jewelry Box': ItemData('Relic', 1337126, useful=True),
|
||||
'Goddess Brooch': ItemData('Relic', 1337127),
|
||||
'Wyrm Brooch': ItemData('Relic', 1337128),
|
||||
'Wyrm Brooch': ItemData('Relic', 1337128),
|
||||
'Greed Brooch': ItemData('Relic', 1337129),
|
||||
'Eternal Brooch': ItemData('Relic', 1337130),
|
||||
'Blue Orb': ItemData('Orb Melee', 1337131),
|
||||
@@ -199,7 +199,11 @@ item_table: Dict[str, ItemData] = {
|
||||
'Chaos Trap': ItemData('Trap', 1337186, 0, trap=True),
|
||||
'Neurotoxin Trap': ItemData('Trap', 1337187, 0, trap=True),
|
||||
'Bee Trap': ItemData('Trap', 1337188, 0, trap=True),
|
||||
# 1337189 - 1337248 Reserved
|
||||
'Laser Access A': ItemData('Relic', 1337189, progression=True),
|
||||
'Laser Access I': ItemData('Relic', 1337191, progression=True),
|
||||
'Laser Access M': ItemData('Relic', 1337192, progression=True),
|
||||
'Throw Stun Trap': ItemData('Trap', 1337193, 0, trap=True),
|
||||
# 1337194 - 1337248 Reserved
|
||||
'Max Sand': ItemData('Stat', 1337249, 14)
|
||||
}
|
||||
|
||||
|
||||
@@ -71,8 +71,8 @@ def get_location_datas(player: Optional[int], options: Optional[TimespinnerOptio
|
||||
LocationData('Skeleton Shaft', 'Sealed Caves (Xarion): Skeleton', 1337044),
|
||||
LocationData('Sealed Caves (Xarion)', 'Sealed Caves (Xarion): Shroom jump room', 1337045, logic.has_timestop),
|
||||
LocationData('Sealed Caves (Xarion)', 'Sealed Caves (Xarion): Double shroom room', 1337046),
|
||||
LocationData('Sealed Caves (Xarion)', 'Sealed Caves (Xarion): Mini jackpot room', 1337047, logic.has_forwarddash_doublejump),
|
||||
LocationData('Sealed Caves (Xarion)', 'Sealed Caves (Xarion): Below mini jackpot room', 1337048),
|
||||
LocationData('Sealed Caves (Xarion)', 'Sealed Caves (Xarion): Jacksquat room', 1337047, logic.has_forwarddash_doublejump),
|
||||
LocationData('Sealed Caves (Xarion)', 'Sealed Caves (Xarion): Below Jacksquat room', 1337048),
|
||||
LocationData('Sealed Caves (Xarion)', 'Sealed Caves (Xarion): Secret room', 1337049, logic.can_break_walls),
|
||||
LocationData('Sealed Caves (Xarion)', 'Sealed Caves (Xarion): Bottom left room', 1337050),
|
||||
LocationData('Sealed Caves (Xarion)', 'Sealed Caves (Xarion): Last chance before Xarion', 1337051, logic.has_doublejump),
|
||||
|
||||
@@ -22,6 +22,7 @@ class TimespinnerLogic:
|
||||
self.flag_specific_keycards = bool(options and options.specific_keycards)
|
||||
self.flag_eye_spy = bool(options and options.eye_spy)
|
||||
self.flag_unchained_keys = bool(options and options.unchained_keys)
|
||||
self.flag_prism_break = bool(options and options.prism_break)
|
||||
|
||||
if precalculated_weights:
|
||||
if self.flag_unchained_keys:
|
||||
@@ -92,6 +93,8 @@ class TimespinnerLogic:
|
||||
return True
|
||||
|
||||
def can_kill_all_3_bosses(self, state: CollectionState) -> bool:
|
||||
if self.flag_prism_break:
|
||||
return state.has_all({'Laser Access M', 'Laser Access I', 'Laser Access A'}, self.player)
|
||||
return state.has_all({'Killed Maw', 'Killed Twins', 'Killed Aelana'}, self.player)
|
||||
|
||||
def has_teleport(self, state: CollectionState) -> bool:
|
||||
|
||||
@@ -180,12 +180,19 @@ class DamageRandoOverrides(OptionDict):
|
||||
}
|
||||
|
||||
class HpCap(Range):
|
||||
"Sets the number that Lunais's HP maxes out at."
|
||||
"""Sets the number that Lunais's HP maxes out at."""
|
||||
display_name = "HP Cap"
|
||||
range_start = 1
|
||||
range_end = 999
|
||||
default = 999
|
||||
|
||||
class AuraCap(Range):
|
||||
"""Sets the maximum Aura Lunais is allowed to have. Level 1 is 80. Djinn Inferno costs 45."""
|
||||
display_name = "Aura Cap"
|
||||
range_start = 45
|
||||
range_end = 999
|
||||
default = 999
|
||||
|
||||
class LevelCap(Range):
|
||||
"""Sets the max level Lunais can achieve."""
|
||||
display_name = "Level Cap"
|
||||
@@ -359,13 +366,18 @@ class TrapChance(Range):
|
||||
class Traps(OptionList):
|
||||
"""List of traps that may be in the item pool to find"""
|
||||
display_name = "Traps Types"
|
||||
valid_keys = { "Meteor Sparrow Trap", "Poison Trap", "Chaos Trap", "Neurotoxin Trap", "Bee Trap" }
|
||||
default = [ "Meteor Sparrow Trap", "Poison Trap", "Chaos Trap", "Neurotoxin Trap", "Bee Trap" ]
|
||||
valid_keys = { "Meteor Sparrow Trap", "Poison Trap", "Chaos Trap", "Neurotoxin Trap", "Bee Trap", "Throw Stun Trap" }
|
||||
default = [ "Meteor Sparrow Trap", "Poison Trap", "Chaos Trap", "Neurotoxin Trap", "Bee Trap", "Throw Stun Trap" ]
|
||||
|
||||
class PresentAccessWithWheelAndSpindle(Toggle):
|
||||
"""When inverted, allows using the refugee camp warp when both the Timespinner Wheel and Spindle is acquired."""
|
||||
display_name = "Back to the future"
|
||||
|
||||
class PrismBreak(Toggle):
|
||||
"""Adds 3 Laser Access items to the item pool to remove the lasers blocking the military hangar area
|
||||
instead of needing to beat the Golden Idol, Aelana, and The Maw."""
|
||||
display_name = "Prism Break"
|
||||
|
||||
@dataclass
|
||||
class TimespinnerOptions(PerGameCommonOptions, DeathLinkMixin):
|
||||
start_with_jewelry_box: StartWithJewelryBox
|
||||
@@ -383,6 +395,7 @@ class TimespinnerOptions(PerGameCommonOptions, DeathLinkMixin):
|
||||
damage_rando: DamageRando
|
||||
damage_rando_overrides: DamageRandoOverrides
|
||||
hp_cap: HpCap
|
||||
aura_cap: AuraCap
|
||||
level_cap: LevelCap
|
||||
extra_earrings_xp: ExtraEarringsXP
|
||||
boss_healing: BossHealing
|
||||
@@ -401,6 +414,7 @@ class TimespinnerOptions(PerGameCommonOptions, DeathLinkMixin):
|
||||
rising_tides_overrides: RisingTidesOverrides
|
||||
unchained_keys: UnchainedKeys
|
||||
back_to_the_future: PresentAccessWithWheelAndSpindle
|
||||
prism_break: PrismBreak
|
||||
trap_chance: TrapChance
|
||||
traps: Traps
|
||||
|
||||
|
||||
@@ -102,6 +102,7 @@ class TimespinnerWorld(World):
|
||||
"DamageRando": self.options.damage_rando.value,
|
||||
"DamageRandoOverrides": self.options.damage_rando_overrides.value,
|
||||
"HpCap": self.options.hp_cap.value,
|
||||
"AuraCap": self.options.aura_cap.value,
|
||||
"LevelCap": self.options.level_cap.value,
|
||||
"ExtraEarringsXP": self.options.extra_earrings_xp.value,
|
||||
"BossHealing": self.options.boss_healing.value,
|
||||
@@ -119,6 +120,7 @@ class TimespinnerWorld(World):
|
||||
"RisingTides": self.options.rising_tides.value,
|
||||
"UnchainedKeys": self.options.unchained_keys.value,
|
||||
"PresentAccessWithWheelAndSpindle": self.options.back_to_the_future.value,
|
||||
"PrismBreak": self.options.prism_break.value,
|
||||
"Traps": self.options.traps.value,
|
||||
"DeathLink": self.options.death_link.value,
|
||||
"StinkyMaw": True,
|
||||
@@ -224,6 +226,9 @@ class TimespinnerWorld(World):
|
||||
elif name in {"Timeworn Warp Beacon", "Modern Warp Beacon", "Mysterious Warp Beacon"} \
|
||||
and not self.options.unchained_keys:
|
||||
item.classification = ItemClassification.filler
|
||||
elif name in {"Laser Access A", "Laser Access I", "Laser Access M"} \
|
||||
and not self.options.prism_break:
|
||||
item.classification = ItemClassification.filler
|
||||
|
||||
return item
|
||||
|
||||
@@ -256,6 +261,11 @@ class TimespinnerWorld(World):
|
||||
excluded_items.add('Modern Warp Beacon')
|
||||
excluded_items.add('Mysterious Warp Beacon')
|
||||
|
||||
if not self.options.prism_break:
|
||||
excluded_items.add('Laser Access A')
|
||||
excluded_items.add('Laser Access I')
|
||||
excluded_items.add('Laser Access M')
|
||||
|
||||
for item in self.multiworld.precollected_items[self.player]:
|
||||
if item.name not in self.item_name_groups['UseItem']:
|
||||
excluded_items.add(item.name)
|
||||
|
||||
@@ -1446,7 +1446,7 @@ def set_er_location_rules(world: "TunicWorld") -> None:
|
||||
set_rule(world.get_location("West Garden Fuse"),
|
||||
lambda state: has_ability(prayer, state, world))
|
||||
set_rule(world.get_location("Library Fuse"),
|
||||
lambda state: has_ability(prayer, state, world))
|
||||
lambda state: has_ability(prayer, state, world) and has_ladder("Ladders in Library", state, world))
|
||||
|
||||
# Bombable Walls
|
||||
for location_name in bomb_walls:
|
||||
|
||||
@@ -752,12 +752,12 @@ Swamp Entry Area (Swamp) - Swamp Sliding Bridge - TrueOneWay:
|
||||
158300 - 0x00987 (Intro Back 7) - 0x00985 - Shapers
|
||||
158301 - 0x181A9 (Intro Back 8) - 0x00987 - Shapers
|
||||
|
||||
Swamp Sliding Bridge (Swamp) - Swamp Entry Area - 0x00609 - Swamp Near Platform - 0x00609:
|
||||
Swamp Sliding Bridge (Swamp) - Swamp Entry Area - 0x00609 - Swamp Platform - 0x00609:
|
||||
158302 - 0x00609 (Sliding Bridge) - True - Shapers
|
||||
159342 - 0x0105D (Sliding Bridge Left EP) - 0x00609 - True
|
||||
159343 - 0x0A304 (Sliding Bridge Right EP) - 0x00609 - True
|
||||
|
||||
Swamp Near Platform (Swamp) - Swamp Cyan Underwater - 0x04B7F - Swamp Near Boat - 0x38AE6 - Swamp Between Bridges Near - 0x184B7 - Swamp Sliding Bridge - TrueOneWay:
|
||||
Swamp Platform (Swamp) - Swamp Cyan Underwater - 0x04B7F - Swamp Near Boat - 0x38AE6 - Swamp Between Bridges Near - 0x184B7 - Swamp Sliding Bridge - TrueOneWay:
|
||||
158313 - 0x00982 (Platform Row 1) - True - Shapers
|
||||
158314 - 0x0097F (Platform Row 2) - 0x00982 - Shapers
|
||||
158315 - 0x0098F (Platform Row 3) - 0x0097F - Shapers
|
||||
|
||||
@@ -752,12 +752,12 @@ Swamp Entry Area (Swamp) - Swamp Sliding Bridge - TrueOneWay:
|
||||
158300 - 0x00987 (Intro Back 7) - 0x00985 - Shapers & Rotated Shapers & Triangles & Black/White Squares
|
||||
158301 - 0x181A9 (Intro Back 8) - 0x00987 - Rotated Shapers & Triangles & Black/White Squares
|
||||
|
||||
Swamp Sliding Bridge (Swamp) - Swamp Entry Area - 0x00609 - Swamp Near Platform - 0x00609:
|
||||
Swamp Sliding Bridge (Swamp) - Swamp Entry Area - 0x00609 - Swamp Platform - 0x00609:
|
||||
158302 - 0x00609 (Sliding Bridge) - True - Shapers & Black/White Squares
|
||||
159342 - 0x0105D (Sliding Bridge Left EP) - 0x00609 - True
|
||||
159343 - 0x0A304 (Sliding Bridge Right EP) - 0x00609 - True
|
||||
|
||||
Swamp Near Platform (Swamp) - Swamp Cyan Underwater - 0x04B7F - Swamp Near Boat - 0x38AE6 - Swamp Between Bridges Near - 0x184B7 - Swamp Sliding Bridge - TrueOneWay:
|
||||
Swamp Platform (Swamp) - Swamp Cyan Underwater - 0x04B7F - Swamp Near Boat - 0x38AE6 - Swamp Between Bridges Near - 0x184B7 - Swamp Sliding Bridge - TrueOneWay:
|
||||
158313 - 0x00982 (Platform Row 1) - True - Rotated Shapers
|
||||
158314 - 0x0097F (Platform Row 2) - 0x00982 - Rotated Shapers
|
||||
158315 - 0x0098F (Platform Row 3) - 0x0097F - Rotated Shapers
|
||||
|
||||
@@ -752,12 +752,12 @@ Swamp Entry Area (Swamp) - Swamp Sliding Bridge - TrueOneWay:
|
||||
158300 - 0x00987 (Intro Back 7) - 0x00985 - Shapers
|
||||
158301 - 0x181A9 (Intro Back 8) - 0x00987 - Shapers
|
||||
|
||||
Swamp Sliding Bridge (Swamp) - Swamp Entry Area - 0x00609 - Swamp Near Platform - 0x00609:
|
||||
Swamp Sliding Bridge (Swamp) - Swamp Entry Area - 0x00609 - Swamp Platform - 0x00609:
|
||||
158302 - 0x00609 (Sliding Bridge) - True - Shapers
|
||||
159342 - 0x0105D (Sliding Bridge Left EP) - 0x00609 - True
|
||||
159343 - 0x0A304 (Sliding Bridge Right EP) - 0x00609 - True
|
||||
|
||||
Swamp Near Platform (Swamp) - Swamp Cyan Underwater - 0x04B7F - Swamp Near Boat - 0x38AE6 - Swamp Between Bridges Near - 0x184B7 - Swamp Sliding Bridge - TrueOneWay:
|
||||
Swamp Platform (Swamp) - Swamp Cyan Underwater - 0x04B7F - Swamp Near Boat - 0x38AE6 - Swamp Between Bridges Near - 0x184B7 - Swamp Sliding Bridge - TrueOneWay:
|
||||
158313 - 0x00982 (Platform Row 1) - True - Shapers
|
||||
158314 - 0x0097F (Platform Row 2) - 0x00982 - Shapers
|
||||
158315 - 0x0098F (Platform Row 3) - 0x0097F - Shapers
|
||||
|
||||
@@ -752,12 +752,12 @@ Swamp Entry Area (Swamp) - Swamp Sliding Bridge - TrueOneWay:
|
||||
158300 - 0x00987 (Intro Back 7) - 0x00985 - Rotated Shapers & Triangles
|
||||
158301 - 0x181A9 (Intro Back 8) - 0x00987 - Shapers & Triangles
|
||||
|
||||
Swamp Sliding Bridge (Swamp) - Swamp Entry Area - 0x00609 - Swamp Near Platform - 0x00609:
|
||||
Swamp Sliding Bridge (Swamp) - Swamp Entry Area - 0x00609 - Swamp Platform - 0x00609:
|
||||
158302 - 0x00609 (Sliding Bridge) - True - Shapers
|
||||
159342 - 0x0105D (Sliding Bridge Left EP) - 0x00609 - True
|
||||
159343 - 0x0A304 (Sliding Bridge Right EP) - 0x00609 - True
|
||||
|
||||
Swamp Near Platform (Swamp) - Swamp Cyan Underwater - 0x04B7F - Swamp Near Boat - 0x38AE6 - Swamp Between Bridges Near - 0x184B7 - Swamp Sliding Bridge - TrueOneWay:
|
||||
Swamp Platform (Swamp) - Swamp Cyan Underwater - 0x04B7F - Swamp Near Boat - 0x38AE6 - Swamp Between Bridges Near - 0x184B7 - Swamp Sliding Bridge - TrueOneWay:
|
||||
158313 - 0x00982 (Platform Row 1) - True - Shapers
|
||||
158314 - 0x0097F (Platform Row 2) - 0x00982 - Shapers
|
||||
158315 - 0x0098F (Platform Row 3) - 0x0097F - Shapers
|
||||
|
||||
@@ -1,11 +0,0 @@
|
||||
New Connections:
|
||||
Quarry - Quarry Elevator - TrueOneWay
|
||||
Outside Quarry - Quarry Elevator - TrueOneWay
|
||||
Outside Bunker - Bunker Elevator - TrueOneWay
|
||||
Outside Swamp - Swamp Long Bridge - TrueOneWay
|
||||
Swamp Near Boat - Swamp Long Bridge - TrueOneWay
|
||||
Town Red Rooftop - Town Maze Rooftop - TrueOneWay
|
||||
|
||||
|
||||
Requirement Changes:
|
||||
0x035DE - 0x17E2B - True
|
||||
@@ -204,10 +204,6 @@ def get_caves_except_path_to_challenge_exclusion_list() -> List[str]:
|
||||
return get_adjustment_file("settings/Exclusions/Caves_Except_Path_To_Challenge.txt")
|
||||
|
||||
|
||||
def get_elevators_come_to_you() -> List[str]:
|
||||
return get_adjustment_file("settings/Door_Shuffle/Elevators_Come_To_You.txt")
|
||||
|
||||
|
||||
def get_entity_hunt() -> List[str]:
|
||||
return get_adjustment_file("settings/Entity_Hunt.txt")
|
||||
|
||||
|
||||
@@ -301,11 +301,11 @@ def hint_from_location(world: "WitnessWorld", location: str) -> Optional[Witness
|
||||
|
||||
def get_item_and_location_names_in_random_order(world: "WitnessWorld",
|
||||
own_itempool: List["WitnessItem"]) -> Tuple[List[str], List[str]]:
|
||||
prog_item_names_in_this_world = [
|
||||
progression_item_names_in_this_world = [
|
||||
item.name for item in own_itempool
|
||||
if item.advancement and item.code and item.location
|
||||
]
|
||||
world.random.shuffle(prog_item_names_in_this_world)
|
||||
world.random.shuffle(progression_item_names_in_this_world)
|
||||
|
||||
locations_in_this_world = [
|
||||
location for location in world.multiworld.get_locations(world.player)
|
||||
@@ -318,22 +318,24 @@ def get_item_and_location_names_in_random_order(world: "WitnessWorld",
|
||||
|
||||
location_names_in_this_world = [location.name for location in locations_in_this_world]
|
||||
|
||||
return prog_item_names_in_this_world, location_names_in_this_world
|
||||
return progression_item_names_in_this_world, location_names_in_this_world
|
||||
|
||||
|
||||
def make_always_and_priority_hints(world: "WitnessWorld", own_itempool: List["WitnessItem"],
|
||||
already_hinted_locations: Set[Location]
|
||||
) -> Tuple[List[WitnessLocationHint], List[WitnessLocationHint]]:
|
||||
|
||||
prog_items_in_this_world, loc_in_this_world = get_item_and_location_names_in_random_order(world, own_itempool)
|
||||
progression_items_in_this_world, locations_in_this_world = get_item_and_location_names_in_random_order(
|
||||
world, own_itempool
|
||||
)
|
||||
|
||||
always_items = [
|
||||
item for item in get_always_hint_items(world)
|
||||
if item in prog_items_in_this_world
|
||||
if item in progression_items_in_this_world
|
||||
]
|
||||
priority_items = [
|
||||
item for item in get_priority_hint_items(world)
|
||||
if item in prog_items_in_this_world
|
||||
if item in progression_items_in_this_world
|
||||
]
|
||||
|
||||
if world.options.vague_hints:
|
||||
@@ -341,11 +343,11 @@ def make_always_and_priority_hints(world: "WitnessWorld", own_itempool: List["Wi
|
||||
else:
|
||||
always_locations = [
|
||||
location for location in get_always_hint_locations(world)
|
||||
if location in loc_in_this_world
|
||||
if location in locations_in_this_world
|
||||
]
|
||||
priority_locations = [
|
||||
location for location in get_priority_hint_locations(world)
|
||||
if location in loc_in_this_world
|
||||
if location in locations_in_this_world
|
||||
]
|
||||
|
||||
# Get always and priority location/item hints
|
||||
@@ -376,7 +378,9 @@ def make_always_and_priority_hints(world: "WitnessWorld", own_itempool: List["Wi
|
||||
def make_extra_location_hints(world: "WitnessWorld", hint_amount: int, own_itempool: List["WitnessItem"],
|
||||
already_hinted_locations: Set[Location], hints_to_use_first: List[WitnessLocationHint],
|
||||
unhinted_locations_for_hinted_areas: Dict[str, Set[Location]]) -> List[WitnessWordedHint]:
|
||||
prog_items_in_this_world, locations_in_this_world = get_item_and_location_names_in_random_order(world, own_itempool)
|
||||
progression_items_in_this_world, locations_in_this_world = get_item_and_location_names_in_random_order(
|
||||
world, own_itempool
|
||||
)
|
||||
|
||||
next_random_hint_is_location = world.random.randrange(0, 2)
|
||||
|
||||
@@ -390,7 +394,7 @@ def make_extra_location_hints(world: "WitnessWorld", hint_amount: int, own_itemp
|
||||
}
|
||||
|
||||
while len(hints) < hint_amount:
|
||||
if not prog_items_in_this_world and not locations_in_this_world and not hints_to_use_first:
|
||||
if not progression_items_in_this_world and not locations_in_this_world and not hints_to_use_first:
|
||||
logging.warning(f"Ran out of items/locations to hint for player {world.player_name}.")
|
||||
break
|
||||
|
||||
@@ -399,8 +403,8 @@ def make_extra_location_hints(world: "WitnessWorld", hint_amount: int, own_itemp
|
||||
location_hint = hints_to_use_first.pop()
|
||||
elif next_random_hint_is_location and locations_in_this_world:
|
||||
location_hint = hint_from_location(world, locations_in_this_world.pop())
|
||||
elif not next_random_hint_is_location and prog_items_in_this_world:
|
||||
location_hint = hint_from_item(world, prog_items_in_this_world.pop(), own_itempool)
|
||||
elif not next_random_hint_is_location and progression_items_in_this_world:
|
||||
location_hint = hint_from_item(world, progression_items_in_this_world.pop(), own_itempool)
|
||||
# The list that the hint was supposed to be taken from was empty.
|
||||
# Try the other list, which has to still have something, as otherwise, all lists would be empty,
|
||||
# which would have triggered the guard condition above.
|
||||
@@ -587,9 +591,11 @@ def make_area_hints(world: "WitnessWorld", amount: int, already_hinted_locations
|
||||
hints = []
|
||||
|
||||
for hinted_area in hinted_areas:
|
||||
hint_string, prog_amount, hunt_panels = word_area_hint(world, hinted_area, items_per_area[hinted_area])
|
||||
hint_string, progression_amount, hunt_panels = word_area_hint(world, hinted_area, items_per_area[hinted_area])
|
||||
|
||||
hints.append(WitnessWordedHint(hint_string, None, f"hinted_area:{hinted_area}", prog_amount, hunt_panels))
|
||||
hints.append(
|
||||
WitnessWordedHint(hint_string, None, f"hinted_area:{hinted_area}", progression_amount, hunt_panels)
|
||||
)
|
||||
|
||||
if len(hinted_areas) < amount:
|
||||
logging.warning(f"Was not able to make {amount} area hints for player {world.player_name}. "
|
||||
|
||||
@@ -2,7 +2,18 @@ from dataclasses import dataclass
|
||||
|
||||
from schema import And, Schema
|
||||
|
||||
from Options import Choice, DefaultOnToggle, OptionDict, OptionGroup, PerGameCommonOptions, Range, Toggle, Visibility
|
||||
from Options import (
|
||||
Choice,
|
||||
DefaultOnToggle,
|
||||
OptionDict,
|
||||
OptionError,
|
||||
OptionGroup,
|
||||
OptionSet,
|
||||
PerGameCommonOptions,
|
||||
Range,
|
||||
Toggle,
|
||||
Visibility,
|
||||
)
|
||||
|
||||
from .data import static_logic as static_witness_logic
|
||||
from .data.item_definition_classes import ItemCategory, WeightedItemDefinition
|
||||
@@ -164,6 +175,16 @@ class ObeliskKeys(DefaultOnToggle):
|
||||
display_name = "Obelisk Keys"
|
||||
|
||||
|
||||
class UnlockableWarps(Toggle):
|
||||
"""
|
||||
Adds unlockable fast travel points to the game.
|
||||
These warp points are represented by spheres in game. You walk up to one, you unlock it for warping.
|
||||
|
||||
The warp points are: Entry, Symmetry Island, Desert, Quarry, Keep, Shipwreck, Town, Jungle, Bunker, Treehouse, Mountaintop, Caves.
|
||||
"""
|
||||
display_name = "Unlockable Fast Travel Points"
|
||||
|
||||
|
||||
class ShufflePostgame(Toggle):
|
||||
"""
|
||||
Adds locations into the pool that are guaranteed to become accessible after or at the same time as your goal.
|
||||
@@ -284,12 +305,33 @@ class ChallengeLasers(Range):
|
||||
default = 11
|
||||
|
||||
|
||||
class ElevatorsComeToYou(Toggle):
|
||||
class ElevatorsComeToYou(OptionSet):
|
||||
"""
|
||||
If on, the Quarry Elevator, Bunker Elevator and Swamp Long Bridge will "come to you" if you approach them.
|
||||
This does actually affect logic as it allows unintended backwards / early access into these areas.
|
||||
In vanilla, some bridges/elevators come to you if you walk up to them when they are not currently there.
|
||||
However, there are some that don't. Notably, this prevents Quarry Elevator from being a logical access method into Quarry, because you can send it away without riding ot and then permanently be locked out of using it.
|
||||
|
||||
This option allows you to change specific elevators/bridges to "come to you" as well.
|
||||
|
||||
- Quarry Elevator: Makes the Quarry Elevator come down when you approach it from lower Quarry and back up when you approach it from above
|
||||
- Swamp Long Bridge: Rotates the side you approach it from towards you, but also rotates the other side away
|
||||
- Bunker Elevator: Makes the Bunker Elevator come to any floor that you approach it from, meaning it can be accessed from the roof immediately
|
||||
"""
|
||||
display_name = "All Bridges & Elevators come to you"
|
||||
|
||||
# Used to be a toggle
|
||||
@classmethod
|
||||
def from_text(cls, text: str):
|
||||
if text.lower() in {"off", "0", "false", "none", "null", "no"}:
|
||||
raise OptionError('elevators_come_to_you is an OptionSet now. The equivalent of "false" is {}')
|
||||
if text.lower() in {"on", "1", "true", "yes"}:
|
||||
raise OptionError(
|
||||
f'elevators_come_to_you is an OptionSet now. The equivalent of "true" is {set(cls.valid_keys)}'
|
||||
)
|
||||
return super().from_text(text)
|
||||
|
||||
display_name = "Elevators come to you"
|
||||
|
||||
valid_keys = frozenset({"Quarry Elevator", "Swamp Long Bridge", "Bunker Elevator"})
|
||||
default = frozenset({"Quarry Elevator"})
|
||||
|
||||
|
||||
class TrapPercentage(Range):
|
||||
@@ -424,6 +466,7 @@ class TheWitnessOptions(PerGameCommonOptions):
|
||||
shuffle_discarded_panels: ShuffleDiscardedPanels
|
||||
shuffle_vault_boxes: ShuffleVaultBoxes
|
||||
obelisk_keys: ObeliskKeys
|
||||
unlockable_warps: UnlockableWarps
|
||||
shuffle_EPs: ShuffleEnvironmentalPuzzles # noqa: N815
|
||||
EP_difficulty: EnvironmentalPuzzlesDifficulty
|
||||
shuffle_postgame: ShufflePostgame
|
||||
@@ -479,6 +522,9 @@ witness_option_groups = [
|
||||
ShuffleBoat,
|
||||
ObeliskKeys,
|
||||
]),
|
||||
OptionGroup("Warps", [
|
||||
UnlockableWarps,
|
||||
]),
|
||||
OptionGroup("Filler Items", [
|
||||
PuzzleSkipAmount,
|
||||
TrapPercentage,
|
||||
|
||||
@@ -55,7 +55,7 @@ class WitnessPlayerItems:
|
||||
name: data for (name, data) in self.item_data.items()
|
||||
if data.classification not in
|
||||
{ItemClassification.progression, ItemClassification.progression_skip_balancing}
|
||||
or name in player_logic.PROG_ITEMS_ACTUALLY_IN_THE_GAME
|
||||
or name in player_logic.PROGRESSION_ITEMS_ACTUALLY_IN_THE_GAME
|
||||
}
|
||||
|
||||
# Downgrade door items
|
||||
@@ -76,7 +76,7 @@ class WitnessPlayerItems:
|
||||
}
|
||||
for item_name, item_data in progression_dict.items():
|
||||
if isinstance(item_data.definition, ProgressiveItemDefinition):
|
||||
num_progression = len(self._logic.MULTI_LISTS[item_name])
|
||||
num_progression = len(self._logic.PROGRESSIVE_LISTS[item_name])
|
||||
self._mandatory_items[item_name] = num_progression
|
||||
else:
|
||||
self._mandatory_items[item_name] = 1
|
||||
|
||||
@@ -34,7 +34,6 @@ from .data.utils import (
|
||||
get_discard_exclusion_list,
|
||||
get_early_caves_list,
|
||||
get_early_caves_start_list,
|
||||
get_elevators_come_to_you,
|
||||
get_entity_hunt,
|
||||
get_ep_all_individual,
|
||||
get_ep_easy,
|
||||
@@ -75,13 +74,15 @@ class WitnessPlayerLogic:
|
||||
|
||||
self.UNREACHABLE_REGIONS: Set[str] = set()
|
||||
|
||||
self.THEORETICAL_BASE_ITEMS: Set[str] = set()
|
||||
self.THEORETICAL_ITEMS: Set[str] = set()
|
||||
self.THEORETICAL_ITEMS_NO_MULTI: Set[str] = set()
|
||||
self.MULTI_AMOUNTS: Dict[str, int] = defaultdict(lambda: 1)
|
||||
self.MULTI_LISTS: Dict[str, List[str]] = {}
|
||||
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI: Set[str] = set()
|
||||
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME: Set[str] = set()
|
||||
self.BASE_PROGESSION_ITEMS_ACTUALLY_IN_THE_GAME: Set[str] = set()
|
||||
self.PROGRESSION_ITEMS_ACTUALLY_IN_THE_GAME: Set[str] = set()
|
||||
|
||||
self.PARENT_ITEM_COUNT_PER_BASE_ITEM: Dict[str, int] = defaultdict(lambda: 1)
|
||||
self.PROGRESSIVE_LISTS: Dict[str, List[str]] = {}
|
||||
self.DOOR_ITEMS_BY_ID: Dict[str, List[str]] = {}
|
||||
|
||||
self.STARTING_INVENTORY: Set[str] = set()
|
||||
|
||||
self.DIFFICULTY = world.options.puzzle_randomization
|
||||
@@ -183,13 +184,13 @@ class WitnessPlayerLogic:
|
||||
|
||||
# Remove any items that don't actually exist in the settings (e.g. Symbol Shuffle turned off)
|
||||
these_items = frozenset({
|
||||
subset.intersection(self.THEORETICAL_ITEMS_NO_MULTI)
|
||||
subset.intersection(self.THEORETICAL_BASE_ITEMS)
|
||||
for subset in these_items
|
||||
})
|
||||
|
||||
# Update the list of "items that are actually being used by any entity"
|
||||
for subset in these_items:
|
||||
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI.update(subset)
|
||||
self.BASE_PROGESSION_ITEMS_ACTUALLY_IN_THE_GAME.update(subset)
|
||||
|
||||
# Handle door entities (door shuffle)
|
||||
if entity_hex in self.DOOR_ITEMS_BY_ID:
|
||||
@@ -197,7 +198,7 @@ class WitnessPlayerLogic:
|
||||
door_items = frozenset({frozenset([item]) for item in self.DOOR_ITEMS_BY_ID[entity_hex]})
|
||||
|
||||
for dependent_item in door_items:
|
||||
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI.update(dependent_item)
|
||||
self.BASE_PROGESSION_ITEMS_ACTUALLY_IN_THE_GAME.update(dependent_item)
|
||||
|
||||
these_items = logical_and_witness_rules([door_items, these_items])
|
||||
|
||||
@@ -299,10 +300,10 @@ class WitnessPlayerLogic:
|
||||
|
||||
self.THEORETICAL_ITEMS.add(item_name)
|
||||
if isinstance(static_witness_logic.ALL_ITEMS[item_name], ProgressiveItemDefinition):
|
||||
self.THEORETICAL_ITEMS_NO_MULTI.update(cast(ProgressiveItemDefinition,
|
||||
static_witness_logic.ALL_ITEMS[item_name]).child_item_names)
|
||||
self.THEORETICAL_BASE_ITEMS.update(cast(ProgressiveItemDefinition,
|
||||
static_witness_logic.ALL_ITEMS[item_name]).child_item_names)
|
||||
else:
|
||||
self.THEORETICAL_ITEMS_NO_MULTI.add(item_name)
|
||||
self.THEORETICAL_BASE_ITEMS.add(item_name)
|
||||
|
||||
if static_witness_logic.ALL_ITEMS[item_name].category in [ItemCategory.DOOR, ItemCategory.LASER]:
|
||||
entity_hexes = cast(DoorItemDefinition, static_witness_logic.ALL_ITEMS[item_name]).panel_id_hexes
|
||||
@@ -316,11 +317,11 @@ class WitnessPlayerLogic:
|
||||
|
||||
self.THEORETICAL_ITEMS.discard(item_name)
|
||||
if isinstance(static_witness_logic.ALL_ITEMS[item_name], ProgressiveItemDefinition):
|
||||
self.THEORETICAL_ITEMS_NO_MULTI.difference_update(
|
||||
self.THEORETICAL_BASE_ITEMS.difference_update(
|
||||
cast(ProgressiveItemDefinition, static_witness_logic.ALL_ITEMS[item_name]).child_item_names
|
||||
)
|
||||
else:
|
||||
self.THEORETICAL_ITEMS_NO_MULTI.discard(item_name)
|
||||
self.THEORETICAL_BASE_ITEMS.discard(item_name)
|
||||
|
||||
if static_witness_logic.ALL_ITEMS[item_name].category in [ItemCategory.DOOR, ItemCategory.LASER]:
|
||||
entity_hexes = cast(DoorItemDefinition, static_witness_logic.ALL_ITEMS[item_name]).panel_id_hexes
|
||||
@@ -624,8 +625,29 @@ class WitnessPlayerLogic:
|
||||
if world.options.early_caves == "add_to_pool" and not remote_doors:
|
||||
adjustment_linesets_in_order.append(get_early_caves_list())
|
||||
|
||||
if world.options.elevators_come_to_you:
|
||||
adjustment_linesets_in_order.append(get_elevators_come_to_you())
|
||||
if "Quarry Elevator" in world.options.elevators_come_to_you:
|
||||
adjustment_linesets_in_order.append([
|
||||
"New Connections:",
|
||||
"Quarry - Quarry Elevator - TrueOneWay",
|
||||
"Outside Quarry - Quarry Elevator - TrueOneWay",
|
||||
])
|
||||
if "Bunker Elevator" in world.options.elevators_come_to_you:
|
||||
adjustment_linesets_in_order.append([
|
||||
"New Connections:",
|
||||
"Outside Bunker - Bunker Elevator - TrueOneWay",
|
||||
])
|
||||
if "Swamp Long Bridge" in world.options.elevators_come_to_you:
|
||||
adjustment_linesets_in_order.append([
|
||||
"New Connections:",
|
||||
"Outside Swamp - Swamp Long Bridge - TrueOneWay",
|
||||
"Swamp Near Boat - Swamp Long Bridge - TrueOneWay",
|
||||
"Requirement Changes:",
|
||||
"0x035DE - 0x17E2B - True", # Swamp Purple Sand Bottom EP
|
||||
])
|
||||
# if "Town Maze Rooftop Bridge" in world.options.elevators_come_to_you:
|
||||
# adjustment_linesets_in_order.append([
|
||||
# "New Connections:"
|
||||
# "Town Red Rooftop - Town Maze Rooftop - TrueOneWay"
|
||||
|
||||
if world.options.victory_condition == "panel_hunt":
|
||||
adjustment_linesets_in_order.append(get_entity_hunt())
|
||||
@@ -843,7 +865,7 @@ class WitnessPlayerLogic:
|
||||
self.REQUIREMENTS_BY_HEX = {}
|
||||
self.USED_EVENT_NAMES_BY_HEX = defaultdict(list)
|
||||
self.CONNECTIONS_BY_REGION_NAME = {}
|
||||
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI = set()
|
||||
self.BASE_PROGESSION_ITEMS_ACTUALLY_IN_THE_GAME = set()
|
||||
|
||||
# Make independent requirements for entities
|
||||
for entity_hex in self.DEPENDENT_REQUIREMENTS_BY_HEX.keys():
|
||||
@@ -868,18 +890,18 @@ class WitnessPlayerLogic:
|
||||
"""
|
||||
Finalise which items are used in the world, and handle their progressive versions.
|
||||
"""
|
||||
for item in self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI:
|
||||
for item in self.BASE_PROGESSION_ITEMS_ACTUALLY_IN_THE_GAME:
|
||||
if item not in self.THEORETICAL_ITEMS:
|
||||
progressive_item_name = static_witness_logic.get_parent_progressive_item(item)
|
||||
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME.add(progressive_item_name)
|
||||
self.PROGRESSION_ITEMS_ACTUALLY_IN_THE_GAME.add(progressive_item_name)
|
||||
child_items = cast(ProgressiveItemDefinition,
|
||||
static_witness_logic.ALL_ITEMS[progressive_item_name]).child_item_names
|
||||
multi_list = [child_item for child_item in child_items
|
||||
if child_item in self.PROG_ITEMS_ACTUALLY_IN_THE_GAME_NO_MULTI]
|
||||
self.MULTI_AMOUNTS[item] = multi_list.index(item) + 1
|
||||
self.MULTI_LISTS[progressive_item_name] = multi_list
|
||||
progressive_list = [child_item for child_item in child_items
|
||||
if child_item in self.BASE_PROGESSION_ITEMS_ACTUALLY_IN_THE_GAME]
|
||||
self.PARENT_ITEM_COUNT_PER_BASE_ITEM[item] = progressive_list.index(item) + 1
|
||||
self.PROGRESSIVE_LISTS[progressive_item_name] = progressive_list
|
||||
else:
|
||||
self.PROG_ITEMS_ACTUALLY_IN_THE_GAME.add(item)
|
||||
self.PROGRESSION_ITEMS_ACTUALLY_IN_THE_GAME.add(item)
|
||||
|
||||
def solvability_guaranteed(self, entity_hex: str) -> bool:
|
||||
return not (
|
||||
|
||||
@@ -35,7 +35,8 @@ witness_option_presets: Dict[str, Dict[str, Any]] = {
|
||||
"challenge_lasers": 11,
|
||||
|
||||
"early_caves": EarlyCaves.option_off,
|
||||
"elevators_come_to_you": False,
|
||||
|
||||
"elevators_come_to_you": ElevatorsComeToYou.default,
|
||||
|
||||
"trap_percentage": TrapPercentage.default,
|
||||
"puzzle_skip_amount": PuzzleSkipAmount.default,
|
||||
@@ -73,7 +74,8 @@ witness_option_presets: Dict[str, Dict[str, Any]] = {
|
||||
"challenge_lasers": 9,
|
||||
|
||||
"early_caves": EarlyCaves.option_off,
|
||||
"elevators_come_to_you": False,
|
||||
|
||||
"elevators_come_to_you": ElevatorsComeToYou.default,
|
||||
|
||||
"trap_percentage": TrapPercentage.default,
|
||||
"puzzle_skip_amount": 15,
|
||||
@@ -111,7 +113,8 @@ witness_option_presets: Dict[str, Dict[str, Any]] = {
|
||||
"challenge_lasers": 9,
|
||||
|
||||
"early_caves": EarlyCaves.option_off,
|
||||
"elevators_come_to_you": True,
|
||||
|
||||
"elevators_come_to_you": ElevatorsComeToYou.valid_keys,
|
||||
|
||||
"trap_percentage": TrapPercentage.default,
|
||||
"puzzle_skip_amount": 15,
|
||||
|
||||
@@ -201,10 +201,10 @@ def _has_item(item: str, world: "WitnessWorld",
|
||||
if item == "Theater to Tunnels":
|
||||
return lambda state: _can_do_theater_to_tunnels(state, world)
|
||||
|
||||
prog_item = static_witness_logic.get_parent_progressive_item(item)
|
||||
needed_amount = player_logic.MULTI_AMOUNTS[item]
|
||||
actual_item = static_witness_logic.get_parent_progressive_item(item)
|
||||
needed_amount = player_logic.PARENT_ITEM_COUNT_PER_BASE_ITEM[item]
|
||||
|
||||
simple_rule: SimpleItemRepresentation = SimpleItemRepresentation(prog_item, needed_amount)
|
||||
simple_rule: SimpleItemRepresentation = SimpleItemRepresentation(actual_item, needed_amount)
|
||||
return simple_rule
|
||||
|
||||
|
||||
|
||||
@@ -1,49 +1,25 @@
|
||||
from ..test import WitnessMultiworldTestBase, WitnessTestBase
|
||||
|
||||
|
||||
class TestElevatorsComeToYou(WitnessTestBase):
|
||||
options = {
|
||||
"elevators_come_to_you": True,
|
||||
"shuffle_doors": "mixed",
|
||||
"shuffle_symbols": False,
|
||||
}
|
||||
|
||||
def test_bunker_laser(self) -> None:
|
||||
"""
|
||||
In elevators_come_to_you, Bunker can be entered from the back.
|
||||
This means that you can access the laser with just Bunker Elevator Control (Panel).
|
||||
It also means that you can, for example, access UV Room with the Control and the Elevator Room Entry Door.
|
||||
"""
|
||||
|
||||
self.assertFalse(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", self.player))
|
||||
|
||||
self.collect_by_name("Bunker Elevator Control (Panel)")
|
||||
|
||||
self.assertTrue(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", self.player))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Bunker UV Room 2", "Location", self.player))
|
||||
|
||||
self.collect_by_name("Bunker Elevator Room Entry (Door)")
|
||||
self.collect_by_name("Bunker Drop-Down Door Controls (Panel)")
|
||||
|
||||
self.assertTrue(self.multiworld.state.can_reach("Bunker UV Room 2", "Location", self.player))
|
||||
from ..test import WitnessMultiworldTestBase
|
||||
|
||||
|
||||
class TestElevatorsComeToYouBleed(WitnessMultiworldTestBase):
|
||||
options_per_world = [
|
||||
{
|
||||
"elevators_come_to_you": False,
|
||||
"elevators_come_to_you": {},
|
||||
},
|
||||
{
|
||||
"elevators_come_to_you": True,
|
||||
"elevators_come_to_you": {"Quarry Elevator", "Swamp Long Bridge", "Bunker Elevator"},
|
||||
},
|
||||
{
|
||||
"elevators_come_to_you": False,
|
||||
"elevators_come_to_you": {}
|
||||
},
|
||||
]
|
||||
|
||||
common_options = {
|
||||
"shuffle_symbols": False,
|
||||
"shuffle_doors": "panels",
|
||||
"shuffle_boat": True,
|
||||
"shuffle_EPs": "individual",
|
||||
"obelisk_keys": False,
|
||||
}
|
||||
|
||||
def test_correct_access_per_player(self) -> None:
|
||||
@@ -53,14 +29,22 @@ class TestElevatorsComeToYouBleed(WitnessMultiworldTestBase):
|
||||
(This is essentially a "does connection info bleed over" test).
|
||||
"""
|
||||
|
||||
self.assertFalse(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", 1))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", 2))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", 3))
|
||||
combinations = [
|
||||
("Quarry Elevator Control (Panel)", "Quarry Boathouse Intro Left"),
|
||||
("Swamp Long Bridge (Panel)", "Swamp Long Bridge Side EP"),
|
||||
("Bunker Elevator Control (Panel)", "Bunker Laser Panel"),
|
||||
]
|
||||
|
||||
self.collect_by_name(["Bunker Elevator Control (Panel)"], 1)
|
||||
self.collect_by_name(["Bunker Elevator Control (Panel)"], 2)
|
||||
self.collect_by_name(["Bunker Elevator Control (Panel)"], 3)
|
||||
for item, location in combinations:
|
||||
with self.subTest(f"Test that {item} only locks {location} for player 2"):
|
||||
self.assertFalse(self.multiworld.state.can_reach_location(location, 1))
|
||||
self.assertFalse(self.multiworld.state.can_reach_location(location, 2))
|
||||
self.assertFalse(self.multiworld.state.can_reach_location(location, 3))
|
||||
|
||||
self.assertFalse(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", 1))
|
||||
self.assertTrue(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", 2))
|
||||
self.assertFalse(self.multiworld.state.can_reach("Bunker Laser Panel", "Location", 3))
|
||||
self.collect_by_name(item, 1)
|
||||
self.collect_by_name(item, 2)
|
||||
self.collect_by_name(item, 3)
|
||||
|
||||
self.assertFalse(self.multiworld.state.can_reach_location(location, 1))
|
||||
self.assertTrue(self.multiworld.state.can_reach_location(location, 2))
|
||||
self.assertFalse(self.multiworld.state.can_reach_location(location, 3))
|
||||
|
||||
@@ -1,3 +1,4 @@
|
||||
from ..options import ElevatorsComeToYou
|
||||
from ..test import WitnessTestBase
|
||||
|
||||
# These are just some random options combinations, just to catch whether I broke anything obvious
|
||||
@@ -19,7 +20,7 @@ class TestExpertNonRandomizedEPs(WitnessTestBase):
|
||||
class TestVanillaAutoElevatorsPanels(WitnessTestBase):
|
||||
options = {
|
||||
"puzzle_randomization": "none",
|
||||
"elevators_come_to_you": True,
|
||||
"elevators_come_to_you": ElevatorsComeToYou.valid_keys - ElevatorsComeToYou.default, # Opposite of default
|
||||
"shuffle_doors": "panels",
|
||||
"victory_condition": "mountain_box_short",
|
||||
"early_caves": True,
|
||||
|
||||
@@ -50,7 +50,7 @@ from .client_bh import YuGiOh2006Client
|
||||
class Yugioh06Web(WebWorld):
|
||||
theme = "stone"
|
||||
setup = Tutorial(
|
||||
"Multiworld Setup Tutorial",
|
||||
"Multiworld Setup Guide",
|
||||
"A guide to setting up Yu-Gi-Oh! - Ultimate Masters Edition - World Championship Tournament 2006 "
|
||||
"for Archipelago on your computer.",
|
||||
"English",
|
||||
|
||||
@@ -3,11 +3,12 @@ from contextlib import redirect_stdout
|
||||
import functools
|
||||
import settings
|
||||
import threading
|
||||
import typing
|
||||
from typing import Any, Dict, List, Set, Tuple, Optional, Union
|
||||
from typing import Any, ClassVar
|
||||
import os
|
||||
import logging
|
||||
|
||||
from typing_extensions import override
|
||||
|
||||
from BaseClasses import ItemClassification, LocationProgressType, \
|
||||
MultiWorld, Item, CollectionState, Entrance, Tutorial
|
||||
|
||||
@@ -47,7 +48,7 @@ class ZillionSettings(settings.Group):
|
||||
"""
|
||||
|
||||
rom_file: RomFile = RomFile(RomFile.copy_to)
|
||||
rom_start: typing.Union[RomStart, bool] = RomStart("retroarch")
|
||||
rom_start: RomStart | bool = RomStart("retroarch")
|
||||
|
||||
|
||||
class ZillionWebWorld(WebWorld):
|
||||
@@ -76,7 +77,7 @@ class ZillionWorld(World):
|
||||
options_dataclass = ZillionOptions
|
||||
options: ZillionOptions # type: ignore
|
||||
|
||||
settings: typing.ClassVar[ZillionSettings] # type: ignore
|
||||
settings: ClassVar[ZillionSettings] # type: ignore
|
||||
# these type: ignore are because of this issue: https://github.com/python/typing/discussions/1486
|
||||
|
||||
topology_present = True # indicate if world type has any meaningful layout/pathing
|
||||
@@ -89,14 +90,14 @@ class ZillionWorld(World):
|
||||
|
||||
class LogStreamInterface:
|
||||
logger: logging.Logger
|
||||
buffer: List[str]
|
||||
buffer: list[str]
|
||||
|
||||
def __init__(self, logger: logging.Logger) -> None:
|
||||
self.logger = logger
|
||||
self.buffer = []
|
||||
|
||||
def write(self, msg: str) -> None:
|
||||
if msg.endswith('\n'):
|
||||
if msg.endswith("\n"):
|
||||
self.buffer.append(msg[:-1])
|
||||
self.logger.debug("".join(self.buffer))
|
||||
self.buffer = []
|
||||
@@ -108,21 +109,21 @@ class ZillionWorld(World):
|
||||
|
||||
lsi: LogStreamInterface
|
||||
|
||||
id_to_zz_item: Optional[Dict[int, ZzItem]] = None
|
||||
id_to_zz_item: dict[int, ZzItem] | None = None
|
||||
zz_system: System
|
||||
_item_counts: "Counter[str]" = Counter()
|
||||
_item_counts: Counter[str] = Counter()
|
||||
"""
|
||||
These are the items counts that will be in the game,
|
||||
which might be different from the item counts the player asked for in options
|
||||
(if the player asked for something invalid).
|
||||
"""
|
||||
my_locations: List[ZillionLocation] = []
|
||||
my_locations: list[ZillionLocation] = []
|
||||
""" This is kind of a cache to avoid iterating through all the multiworld locations in logic. """
|
||||
slot_data_ready: threading.Event
|
||||
""" This event is set in `generate_output` when the data is ready for `fill_slot_data` """
|
||||
logic_cache: Union[ZillionLogicCache, None] = None
|
||||
logic_cache: ZillionLogicCache | None = None
|
||||
|
||||
def __init__(self, world: MultiWorld, player: int):
|
||||
def __init__(self, world: MultiWorld, player: int) -> None:
|
||||
super().__init__(world, player)
|
||||
self.logger = logging.getLogger("Zillion")
|
||||
self.lsi = ZillionWorld.LogStreamInterface(self.logger)
|
||||
@@ -133,6 +134,7 @@ class ZillionWorld(World):
|
||||
_id_to_name, _id_to_zz_id, id_to_zz_item = make_id_to_others(start_char)
|
||||
self.id_to_zz_item = id_to_zz_item
|
||||
|
||||
@override
|
||||
def generate_early(self) -> None:
|
||||
zz_op, item_counts = validate(self.options)
|
||||
|
||||
@@ -150,12 +152,13 @@ class ZillionWorld(World):
|
||||
# just in case the options changed anything (I don't think they do)
|
||||
assert self.zz_system.randomizer, "init failed"
|
||||
for zz_name in self.zz_system.randomizer.locations:
|
||||
if zz_name != 'main':
|
||||
if zz_name != "main":
|
||||
assert self.zz_system.randomizer.loc_name_2_pretty[zz_name] in self.location_name_to_id, \
|
||||
f"{self.zz_system.randomizer.loc_name_2_pretty[zz_name]} not in location map"
|
||||
|
||||
self._make_item_maps(zz_op.start_char)
|
||||
|
||||
@override
|
||||
def create_regions(self) -> None:
|
||||
assert self.zz_system.randomizer, "generate_early hasn't been called"
|
||||
assert self.id_to_zz_item, "generate_early hasn't been called"
|
||||
@@ -177,23 +180,23 @@ class ZillionWorld(World):
|
||||
zz_loc.req.gun = 1
|
||||
assert len(self.zz_system.randomizer.get_locations(Req(gun=1, jump=1))) != 0
|
||||
|
||||
start = self.zz_system.randomizer.regions['start']
|
||||
start = self.zz_system.randomizer.regions["start"]
|
||||
|
||||
all: Dict[str, ZillionRegion] = {}
|
||||
all_regions: dict[str, ZillionRegion] = {}
|
||||
for here_zz_name, zz_r in self.zz_system.randomizer.regions.items():
|
||||
here_name = "Menu" if here_zz_name == "start" else zz_reg_name_to_reg_name(here_zz_name)
|
||||
all[here_name] = ZillionRegion(zz_r, here_name, here_name, p, w)
|
||||
self.multiworld.regions.append(all[here_name])
|
||||
all_regions[here_name] = ZillionRegion(zz_r, here_name, here_name, p, w)
|
||||
self.multiworld.regions.append(all_regions[here_name])
|
||||
|
||||
limited_skill = Req(gun=3, jump=3, skill=self.zz_system.randomizer.options.skill, hp=940, red=1, floppy=126)
|
||||
queue = deque([start])
|
||||
done: Set[str] = set()
|
||||
done: set[str] = set()
|
||||
while len(queue):
|
||||
zz_here = queue.popleft()
|
||||
here_name = "Menu" if zz_here.name == "start" else zz_reg_name_to_reg_name(zz_here.name)
|
||||
if here_name in done:
|
||||
continue
|
||||
here = all[here_name]
|
||||
here = all_regions[here_name]
|
||||
|
||||
for zz_loc in zz_here.locations:
|
||||
# if local gun reqs didn't place "keyword" item
|
||||
@@ -217,15 +220,16 @@ class ZillionWorld(World):
|
||||
self.my_locations.append(loc)
|
||||
|
||||
for zz_dest in zz_here.connections.keys():
|
||||
dest_name = "Menu" if zz_dest.name == 'start' else zz_reg_name_to_reg_name(zz_dest.name)
|
||||
dest = all[dest_name]
|
||||
exit = Entrance(p, f"{here_name} to {dest_name}", here)
|
||||
here.exits.append(exit)
|
||||
exit.connect(dest)
|
||||
dest_name = "Menu" if zz_dest.name == "start" else zz_reg_name_to_reg_name(zz_dest.name)
|
||||
dest = all_regions[dest_name]
|
||||
exit_ = Entrance(p, f"{here_name} to {dest_name}", here)
|
||||
here.exits.append(exit_)
|
||||
exit_.connect(dest)
|
||||
|
||||
queue.append(zz_dest)
|
||||
done.add(here.name)
|
||||
|
||||
@override
|
||||
def create_items(self) -> None:
|
||||
if not self.id_to_zz_item:
|
||||
self._make_item_maps("JJ")
|
||||
@@ -249,14 +253,11 @@ class ZillionWorld(World):
|
||||
self.logger.debug(f"Zillion Items: {item_name} 1")
|
||||
self.multiworld.itempool.append(self.create_item(item_name))
|
||||
|
||||
def set_rules(self) -> None:
|
||||
# logic for this game is in create_regions
|
||||
pass
|
||||
|
||||
@override
|
||||
def generate_basic(self) -> None:
|
||||
assert self.zz_system.randomizer, "generate_early hasn't been called"
|
||||
# main location name is an alias
|
||||
main_loc_name = self.zz_system.randomizer.loc_name_2_pretty[self.zz_system.randomizer.locations['main'].name]
|
||||
main_loc_name = self.zz_system.randomizer.loc_name_2_pretty[self.zz_system.randomizer.locations["main"].name]
|
||||
|
||||
self.multiworld.get_location(main_loc_name, self.player)\
|
||||
.place_locked_item(self.create_item("Win"))
|
||||
@@ -264,22 +265,18 @@ class ZillionWorld(World):
|
||||
lambda state: state.has("Win", self.player)
|
||||
|
||||
@staticmethod
|
||||
def stage_generate_basic(multiworld: MultiWorld, *args: Any) -> None:
|
||||
def stage_generate_basic(multiworld: MultiWorld, *args: Any) -> None: # noqa: ANN401
|
||||
# item link pools are about to be created in main
|
||||
# JJ can't be an item link unless all the players share the same start_char
|
||||
# (The reason for this is that the JJ ZillionItem will have a different ZzItem depending
|
||||
# on whether the start char is Apple or Champ, and the logic depends on that ZzItem.)
|
||||
for group in multiworld.groups.values():
|
||||
# TODO: remove asserts on group when we can specify which members of TypedDict are optional
|
||||
assert "game" in group
|
||||
if group["game"] == "Zillion":
|
||||
assert "item_pool" in group
|
||||
if group["game"] == "Zillion" and "item_pool" in group:
|
||||
item_pool = group["item_pool"]
|
||||
to_stay: Chars = "JJ"
|
||||
if "JJ" in item_pool:
|
||||
assert "players" in group
|
||||
group_players = group["players"]
|
||||
players_start_chars: List[Tuple[int, Chars]] = []
|
||||
group["players"] = group_players = set(group["players"])
|
||||
players_start_chars: list[tuple[int, Chars]] = []
|
||||
for player in group_players:
|
||||
z_world = multiworld.worlds[player]
|
||||
assert isinstance(z_world, ZillionWorld)
|
||||
@@ -291,17 +288,17 @@ class ZillionWorld(World):
|
||||
elif start_char_counts["Champ"] > start_char_counts["Apple"]:
|
||||
to_stay = "Champ"
|
||||
else: # equal
|
||||
choices: Tuple[Chars, ...] = ("Apple", "Champ")
|
||||
choices: tuple[Chars, ...] = ("Apple", "Champ")
|
||||
to_stay = multiworld.random.choice(choices)
|
||||
|
||||
for p, sc in players_start_chars:
|
||||
if sc != to_stay:
|
||||
group_players.remove(p)
|
||||
assert "world" in group
|
||||
group_world = group["world"]
|
||||
assert isinstance(group_world, ZillionWorld)
|
||||
group_world._make_item_maps(to_stay)
|
||||
|
||||
@override
|
||||
def post_fill(self) -> None:
|
||||
"""Optional Method that is called after regular fill. Can be used to do adjustments before output generation.
|
||||
This happens before progression balancing, so the items may not be in their final locations yet."""
|
||||
@@ -317,10 +314,10 @@ class ZillionWorld(World):
|
||||
|
||||
assert self.zz_system.randomizer, "generate_early hasn't been called"
|
||||
|
||||
# debug_zz_loc_ids: Dict[str, int] = {}
|
||||
# debug_zz_loc_ids: dict[str, int] = {}
|
||||
empty = zz_items[4]
|
||||
multi_item = empty # a different patcher method differentiates empty from ap multi item
|
||||
multi_items: Dict[str, Tuple[str, str]] = {} # zz_loc_name to (item_name, player_name)
|
||||
multi_items: dict[str, tuple[str, str]] = {} # zz_loc_name to (item_name, player_name)
|
||||
for z_loc in self.multiworld.get_locations(self.player):
|
||||
assert isinstance(z_loc, ZillionLocation)
|
||||
# debug_zz_loc_ids[z_loc.zz_loc.name] = id(z_loc.zz_loc)
|
||||
@@ -343,7 +340,7 @@ class ZillionWorld(World):
|
||||
# print(id_)
|
||||
# print("size:", len(debug_zz_loc_ids))
|
||||
|
||||
# debug_loc_to_id: Dict[str, int] = {}
|
||||
# debug_loc_to_id: dict[str, int] = {}
|
||||
# regions = self.zz_randomizer.regions
|
||||
# for region in regions.values():
|
||||
# for loc in region.locations:
|
||||
@@ -358,10 +355,11 @@ class ZillionWorld(World):
|
||||
f"in world {self.player} didn't get an item"
|
||||
)
|
||||
|
||||
game_id = self.multiworld.player_name[self.player].encode() + b'\x00' + self.multiworld.seed_name[-6:].encode()
|
||||
game_id = self.multiworld.player_name[self.player].encode() + b"\x00" + self.multiworld.seed_name[-6:].encode()
|
||||
|
||||
return GenData(multi_items, self.zz_system.get_game(), game_id)
|
||||
|
||||
@override
|
||||
def generate_output(self, output_directory: str) -> None:
|
||||
"""This method gets called from a threadpool, do not use multiworld.random here.
|
||||
If you need any last-second randomization, use self.random instead."""
|
||||
@@ -383,6 +381,7 @@ class ZillionWorld(World):
|
||||
|
||||
self.logger.debug(f"Zillion player {self.player} finished generate_output")
|
||||
|
||||
@override
|
||||
def fill_slot_data(self) -> ZillionSlotInfo: # json of WebHostLib.models.Slot
|
||||
"""Fill in the `slot_data` field in the `Connected` network package.
|
||||
This is a way the generator can give custom data to the client.
|
||||
@@ -400,6 +399,7 @@ class ZillionWorld(World):
|
||||
|
||||
# end of ordered Main.py calls
|
||||
|
||||
@override
|
||||
def create_item(self, name: str) -> Item:
|
||||
"""Create an item for this world type and player.
|
||||
Warning: this may be called with self.multiworld = None, for example by MultiServer"""
|
||||
@@ -420,6 +420,7 @@ class ZillionWorld(World):
|
||||
z_item = ZillionItem(name, classification, item_id, self.player, zz_item)
|
||||
return z_item
|
||||
|
||||
@override
|
||||
def get_filler_item_name(self) -> str:
|
||||
"""Called when the item pool needs to be filled with additional items to match location count."""
|
||||
return "Empty"
|
||||
|
||||
@@ -3,7 +3,7 @@ import base64
|
||||
import io
|
||||
import pkgutil
|
||||
import platform
|
||||
from typing import Any, ClassVar, Coroutine, Dict, List, Optional, Protocol, Tuple, cast
|
||||
from typing import Any, ClassVar, Coroutine, Protocol, cast
|
||||
|
||||
from CommonClient import CommonContext, server_loop, gui_enabled, \
|
||||
ClientCommandProcessor, logger, get_base_parser
|
||||
@@ -11,6 +11,7 @@ from NetUtils import ClientStatus
|
||||
from Utils import async_start
|
||||
|
||||
import colorama
|
||||
from typing_extensions import override
|
||||
|
||||
from zilliandomizer.zri.memory import Memory, RescueInfo
|
||||
from zilliandomizer.zri import events
|
||||
@@ -35,11 +36,11 @@ class ZillionCommandProcessor(ClientCommandProcessor):
|
||||
|
||||
|
||||
class ToggleCallback(Protocol):
|
||||
def __call__(self) -> None: ...
|
||||
def __call__(self) -> object: ...
|
||||
|
||||
|
||||
class SetRoomCallback(Protocol):
|
||||
def __call__(self, rooms: List[List[int]]) -> None: ...
|
||||
def __call__(self, rooms: list[list[int]]) -> object: ...
|
||||
|
||||
|
||||
class ZillionContext(CommonContext):
|
||||
@@ -47,7 +48,7 @@ class ZillionContext(CommonContext):
|
||||
command_processor = ZillionCommandProcessor
|
||||
items_handling = 1 # receive items from other players
|
||||
|
||||
known_name: Optional[str]
|
||||
known_name: str | None
|
||||
""" This is almost the same as `auth` except `auth` is reset to `None` when server disconnects, and this isn't. """
|
||||
|
||||
from_game: "asyncio.Queue[events.EventFromGame]"
|
||||
@@ -56,11 +57,11 @@ class ZillionContext(CommonContext):
|
||||
""" local checks watched by server """
|
||||
next_item: int
|
||||
""" index in `items_received` """
|
||||
ap_id_to_name: Dict[int, str]
|
||||
ap_id_to_zz_id: Dict[int, int]
|
||||
ap_id_to_name: dict[int, str]
|
||||
ap_id_to_zz_id: dict[int, int]
|
||||
start_char: Chars = "JJ"
|
||||
rescues: Dict[int, RescueInfo] = {}
|
||||
loc_mem_to_id: Dict[int, int] = {}
|
||||
rescues: dict[int, RescueInfo] = {}
|
||||
loc_mem_to_id: dict[int, int] = {}
|
||||
got_room_info: asyncio.Event
|
||||
""" flag for connected to server """
|
||||
got_slot_data: asyncio.Event
|
||||
@@ -119,22 +120,22 @@ class ZillionContext(CommonContext):
|
||||
self.finished_game = False
|
||||
self.items_received.clear()
|
||||
|
||||
# override
|
||||
def on_deathlink(self, data: Dict[str, Any]) -> None:
|
||||
@override
|
||||
def on_deathlink(self, data: dict[str, Any]) -> None:
|
||||
self.to_game.put_nowait(events.DeathEventToGame())
|
||||
return super().on_deathlink(data)
|
||||
|
||||
# override
|
||||
@override
|
||||
async def server_auth(self, password_requested: bool = False) -> None:
|
||||
if password_requested and not self.password:
|
||||
await super().server_auth(password_requested)
|
||||
if not self.auth:
|
||||
logger.info('waiting for connection to game...')
|
||||
logger.info("waiting for connection to game...")
|
||||
return
|
||||
logger.info("logging in to server...")
|
||||
await self.send_connect()
|
||||
|
||||
# override
|
||||
@override
|
||||
def run_gui(self) -> None:
|
||||
from kvui import GameManager
|
||||
from kivy.core.text import Label as CoreLabel
|
||||
@@ -154,10 +155,10 @@ class ZillionContext(CommonContext):
|
||||
MAP_WIDTH: ClassVar[int] = 281
|
||||
|
||||
map_background: CoreImage
|
||||
_number_textures: List[Texture] = []
|
||||
rooms: List[List[int]] = []
|
||||
_number_textures: list[Texture] = []
|
||||
rooms: list[list[int]] = []
|
||||
|
||||
def __init__(self, **kwargs: Any) -> None:
|
||||
def __init__(self, **kwargs: Any) -> None: # noqa: ANN401
|
||||
super().__init__(**kwargs)
|
||||
|
||||
FILE_NAME = "empty-zillion-map-row-col-labels-281.png"
|
||||
@@ -183,7 +184,7 @@ class ZillionContext(CommonContext):
|
||||
label.refresh()
|
||||
self._number_textures.append(label.texture)
|
||||
|
||||
def update_map(self, *args: Any) -> None:
|
||||
def update_map(self, *args: Any) -> None: # noqa: ANN401
|
||||
self.canvas.clear()
|
||||
|
||||
with self.canvas:
|
||||
@@ -203,6 +204,7 @@ class ZillionContext(CommonContext):
|
||||
num_texture = self._number_textures[num]
|
||||
Rectangle(texture=num_texture, size=num_texture.size, pos=pos)
|
||||
|
||||
@override
|
||||
def build(self) -> Layout:
|
||||
container = super().build()
|
||||
self.map_widget = ZillionManager.MapPanel(size_hint_x=None, width=ZillionManager.MapPanel.MAP_WIDTH)
|
||||
@@ -216,17 +218,18 @@ class ZillionContext(CommonContext):
|
||||
self.map_widget.width = 0
|
||||
self.container.do_layout()
|
||||
|
||||
def set_rooms(self, rooms: List[List[int]]) -> None:
|
||||
def set_rooms(self, rooms: list[list[int]]) -> None:
|
||||
self.map_widget.rooms = rooms
|
||||
self.map_widget.update_map()
|
||||
|
||||
self.ui = ZillionManager(self)
|
||||
self.ui_toggle_map = lambda: self.ui.toggle_map_width()
|
||||
self.ui_set_rooms = lambda rooms: self.ui.set_rooms(rooms)
|
||||
self.ui_toggle_map = lambda: isinstance(self.ui, ZillionManager) and self.ui.toggle_map_width()
|
||||
self.ui_set_rooms = lambda rooms: isinstance(self.ui, ZillionManager) and self.ui.set_rooms(rooms)
|
||||
run_co: Coroutine[Any, Any, None] = self.ui.async_run()
|
||||
self.ui_task = asyncio.create_task(run_co, name="UI")
|
||||
|
||||
def on_package(self, cmd: str, args: Dict[str, Any]) -> None:
|
||||
@override
|
||||
def on_package(self, cmd: str, args: dict[str, Any]) -> None:
|
||||
self.room_item_numbers_to_ui()
|
||||
if cmd == "Connected":
|
||||
logger.info("logged in to Archipelago server")
|
||||
@@ -238,7 +241,7 @@ class ZillionContext(CommonContext):
|
||||
if "start_char" not in slot_data:
|
||||
logger.warning("invalid Zillion `Connected` packet, `slot_data` missing `start_char`")
|
||||
return
|
||||
self.start_char = slot_data['start_char']
|
||||
self.start_char = slot_data["start_char"]
|
||||
if self.start_char not in {"Apple", "Champ", "JJ"}:
|
||||
logger.warning("invalid Zillion `Connected` packet, "
|
||||
f"`slot_data` `start_char` has invalid value: {self.start_char}")
|
||||
@@ -259,7 +262,7 @@ class ZillionContext(CommonContext):
|
||||
self.rescues[0 if rescue_id == "0" else 1] = ri
|
||||
|
||||
if "loc_mem_to_id" not in slot_data:
|
||||
logger.warn("invalid Zillion `Connected` packet, `slot_data` missing `loc_mem_to_id`")
|
||||
logger.warning("invalid Zillion `Connected` packet, `slot_data` missing `loc_mem_to_id`")
|
||||
return
|
||||
loc_mem_to_id = slot_data["loc_mem_to_id"]
|
||||
self.loc_mem_to_id = {}
|
||||
@@ -286,7 +289,7 @@ class ZillionContext(CommonContext):
|
||||
if "keys" not in args:
|
||||
logger.warning(f"invalid Retrieved packet to ZillionClient: {args}")
|
||||
return
|
||||
keys = cast(Dict[str, Optional[str]], args["keys"])
|
||||
keys = cast(dict[str, str | None], args["keys"])
|
||||
doors_b64 = keys.get(f"zillion-{self.auth}-doors", None)
|
||||
if doors_b64:
|
||||
logger.info("received door data from server")
|
||||
@@ -321,9 +324,9 @@ class ZillionContext(CommonContext):
|
||||
if server_id in self.missing_locations:
|
||||
self.ap_local_count += 1
|
||||
n_locations = len(self.missing_locations) + len(self.checked_locations) - 1 # -1 to ignore win
|
||||
logger.info(f'New Check: {loc_name} ({self.ap_local_count}/{n_locations})')
|
||||
logger.info(f"New Check: {loc_name} ({self.ap_local_count}/{n_locations})")
|
||||
async_start(self.send_msgs([
|
||||
{"cmd": 'LocationChecks', "locations": [server_id]}
|
||||
{"cmd": "LocationChecks", "locations": [server_id]}
|
||||
]))
|
||||
else:
|
||||
# This will happen a lot in Zillion,
|
||||
@@ -334,7 +337,7 @@ class ZillionContext(CommonContext):
|
||||
elif isinstance(event_from_game, events.WinEventFromGame):
|
||||
if not self.finished_game:
|
||||
async_start(self.send_msgs([
|
||||
{"cmd": 'LocationChecks', "locations": [loc_name_to_id["J-6 bottom far left"]]},
|
||||
{"cmd": "LocationChecks", "locations": [loc_name_to_id["J-6 bottom far left"]]},
|
||||
{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}
|
||||
]))
|
||||
self.finished_game = True
|
||||
@@ -362,24 +365,24 @@ class ZillionContext(CommonContext):
|
||||
ap_id = self.items_received[index].item
|
||||
from_name = self.player_names[self.items_received[index].player]
|
||||
# TODO: colors in this text, like sni client?
|
||||
logger.info(f'received {self.ap_id_to_name[ap_id]} from {from_name}')
|
||||
logger.info(f"received {self.ap_id_to_name[ap_id]} from {from_name}")
|
||||
self.to_game.put_nowait(
|
||||
events.ItemEventToGame(zz_item_ids)
|
||||
)
|
||||
self.next_item = len(self.items_received)
|
||||
|
||||
|
||||
def name_seed_from_ram(data: bytes) -> Tuple[str, str]:
|
||||
def name_seed_from_ram(data: bytes) -> tuple[str, str]:
|
||||
""" returns player name, and end of seed string """
|
||||
if len(data) == 0:
|
||||
# no connection to game
|
||||
return "", "xxx"
|
||||
null_index = data.find(b'\x00')
|
||||
null_index = data.find(b"\x00")
|
||||
if null_index == -1:
|
||||
logger.warning(f"invalid game id in rom {repr(data)}")
|
||||
null_index = len(data)
|
||||
name = data[:null_index].decode()
|
||||
null_index_2 = data.find(b'\x00', null_index + 1)
|
||||
null_index_2 = data.find(b"\x00", null_index + 1)
|
||||
if null_index_2 == -1:
|
||||
null_index_2 = len(data)
|
||||
seed_name = data[null_index + 1:null_index_2].decode()
|
||||
@@ -479,8 +482,8 @@ async def zillion_sync_task(ctx: ZillionContext) -> None:
|
||||
|
||||
async def main() -> None:
|
||||
parser = get_base_parser()
|
||||
parser.add_argument('diff_file', default="", type=str, nargs="?",
|
||||
help='Path to a .apzl Archipelago Binary Patch file')
|
||||
parser.add_argument("diff_file", default="", type=str, nargs="?",
|
||||
help="Path to a .apzl Archipelago Binary Patch file")
|
||||
# SNI parser.add_argument('--loglevel', default='info', choices=['debug', 'info', 'warning', 'error', 'critical'])
|
||||
args = parser.parse_args()
|
||||
print(args)
|
||||
|
||||
@@ -1,6 +1,5 @@
|
||||
from dataclasses import dataclass
|
||||
import json
|
||||
from typing import Dict, Tuple
|
||||
|
||||
from zilliandomizer.game import Game as ZzGame
|
||||
|
||||
@@ -9,7 +8,7 @@ from zilliandomizer.game import Game as ZzGame
|
||||
class GenData:
|
||||
""" data passed from generation to patcher """
|
||||
|
||||
multi_items: Dict[str, Tuple[str, str]]
|
||||
multi_items: dict[str, tuple[str, str]]
|
||||
""" zz_loc_name to (item_name, player_name) """
|
||||
zz_game: ZzGame
|
||||
game_id: bytes
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
from collections import defaultdict
|
||||
from typing import Dict, Iterable, Mapping, Tuple, TypedDict
|
||||
from collections.abc import Iterable, Mapping
|
||||
from typing import TypedDict
|
||||
|
||||
from zilliandomizer.logic_components.items import (
|
||||
Item as ZzItem,
|
||||
@@ -40,13 +41,13 @@ _zz_rescue_1 = zz_item_name_to_zz_item["rescue_1"]
|
||||
_zz_empty = zz_item_name_to_zz_item["empty"]
|
||||
|
||||
|
||||
def make_id_to_others(start_char: Chars) -> Tuple[
|
||||
Dict[int, str], Dict[int, int], Dict[int, ZzItem]
|
||||
def make_id_to_others(start_char: Chars) -> tuple[
|
||||
dict[int, str], dict[int, int], dict[int, ZzItem]
|
||||
]:
|
||||
""" returns id_to_name, id_to_zz_id, id_to_zz_item """
|
||||
id_to_name: Dict[int, str] = {}
|
||||
id_to_zz_id: Dict[int, int] = {}
|
||||
id_to_zz_item: Dict[int, ZzItem] = {}
|
||||
id_to_name: dict[int, str] = {}
|
||||
id_to_zz_id: dict[int, int] = {}
|
||||
id_to_zz_item: dict[int, ZzItem] = {}
|
||||
|
||||
if start_char == "JJ":
|
||||
name_to_zz_item = {
|
||||
@@ -91,14 +92,14 @@ def make_room_name(row: int, col: int) -> str:
|
||||
return f"{chr(ord('A') + row - 1)}-{col + 1}"
|
||||
|
||||
|
||||
loc_name_to_id: Dict[str, int] = {
|
||||
loc_name_to_id: dict[str, int] = {
|
||||
name: id_ + base_id
|
||||
for name, id_ in pretty_loc_name_to_id.items()
|
||||
}
|
||||
|
||||
|
||||
def zz_reg_name_to_reg_name(zz_reg_name: str) -> str:
|
||||
if zz_reg_name[0] == 'r' and zz_reg_name[3] == 'c':
|
||||
if zz_reg_name[0] == "r" and zz_reg_name[3] == "c":
|
||||
row, col = parse_reg_name(zz_reg_name)
|
||||
end = zz_reg_name[5:]
|
||||
return f"{make_room_name(row, col)} {end.upper()}"
|
||||
@@ -113,17 +114,17 @@ class ClientRescue(TypedDict):
|
||||
|
||||
class ZillionSlotInfo(TypedDict):
|
||||
start_char: Chars
|
||||
rescues: Dict[str, ClientRescue]
|
||||
loc_mem_to_id: Dict[int, int]
|
||||
rescues: dict[str, ClientRescue]
|
||||
loc_mem_to_id: dict[int, int]
|
||||
""" memory location of canister to Archipelago location id number """
|
||||
|
||||
|
||||
def get_slot_info(regions: Iterable[RegionData],
|
||||
start_char: Chars,
|
||||
loc_name_to_pretty: Mapping[str, str]) -> ZillionSlotInfo:
|
||||
items_placed_in_map_index: Dict[int, int] = defaultdict(int)
|
||||
rescue_locations: Dict[int, RescueInfo] = {}
|
||||
loc_memory_to_loc_id: Dict[int, int] = {}
|
||||
items_placed_in_map_index: dict[int, int] = defaultdict(int)
|
||||
rescue_locations: dict[int, RescueInfo] = {}
|
||||
loc_memory_to_loc_id: dict[int, int] = {}
|
||||
for region in regions:
|
||||
for loc in region.locations:
|
||||
assert loc.item, ("There should be an item placed in every location before "
|
||||
@@ -142,7 +143,7 @@ def get_slot_info(regions: Iterable[RegionData],
|
||||
loc_memory_to_loc_id[loc_memory] = pretty_loc_name_to_id[loc_name_to_pretty[loc.name]]
|
||||
items_placed_in_map_index[map_index] += 1
|
||||
|
||||
rescues: Dict[str, ClientRescue] = {}
|
||||
rescues: dict[str, ClientRescue] = {}
|
||||
for i in (0, 1):
|
||||
if i in rescue_locations:
|
||||
ri = rescue_locations[i]
|
||||
|
||||
@@ -1,4 +1,5 @@
|
||||
from typing import Dict, FrozenSet, Mapping, Tuple, List, Counter as _Counter
|
||||
from collections import Counter
|
||||
from collections.abc import Mapping
|
||||
|
||||
from BaseClasses import CollectionState
|
||||
|
||||
@@ -35,7 +36,7 @@ def set_randomizer_locs(cs: CollectionState, p: int, zz_r: Randomizer) -> int:
|
||||
return _hash
|
||||
|
||||
|
||||
def item_counts(cs: CollectionState, p: int) -> Tuple[Tuple[str, int], ...]:
|
||||
def item_counts(cs: CollectionState, p: int) -> tuple[tuple[str, int], ...]:
|
||||
"""
|
||||
the zilliandomizer items that player p has collected
|
||||
|
||||
@@ -44,11 +45,11 @@ def item_counts(cs: CollectionState, p: int) -> Tuple[Tuple[str, int], ...]:
|
||||
return tuple((item_name, cs.count(item_name, p)) for item_name in item_name_to_id)
|
||||
|
||||
|
||||
_cache_miss: Tuple[None, FrozenSet[Location]] = (None, frozenset())
|
||||
_cache_miss: tuple[None, frozenset[Location]] = (None, frozenset())
|
||||
|
||||
|
||||
class ZillionLogicCache:
|
||||
_cache: Dict[int, Tuple[_Counter[str], FrozenSet[Location]]]
|
||||
_cache: dict[int, tuple[Counter[str], frozenset[Location]]]
|
||||
""" `{ hash: (counter_from_prog_items, accessible_zz_locations) }` """
|
||||
_player: int
|
||||
_zz_r: Randomizer
|
||||
@@ -60,7 +61,7 @@ class ZillionLogicCache:
|
||||
self._zz_r = zz_r
|
||||
self._id_to_zz_item = id_to_zz_item
|
||||
|
||||
def cs_to_zz_locs(self, cs: CollectionState) -> FrozenSet[Location]:
|
||||
def cs_to_zz_locs(self, cs: CollectionState) -> frozenset[Location]:
|
||||
"""
|
||||
given an Archipelago `CollectionState`,
|
||||
returns frozenset of accessible zilliandomizer locations
|
||||
@@ -76,7 +77,7 @@ class ZillionLogicCache:
|
||||
return locs
|
||||
|
||||
# print("cache miss")
|
||||
have_items: List[Item] = []
|
||||
have_items: list[Item] = []
|
||||
for name, count in counts:
|
||||
have_items.extend([self._id_to_zz_item[item_name_to_id[name]]] * count)
|
||||
# have_req is the result of converting AP CollectionState to zilliandomizer collection state
|
||||
|
||||
@@ -1,6 +1,6 @@
|
||||
from collections import Counter
|
||||
from dataclasses import dataclass
|
||||
from typing import ClassVar, Dict, Literal, Tuple, TypeGuard
|
||||
from typing import ClassVar, Literal, TypeGuard
|
||||
|
||||
from Options import Choice, DefaultOnToggle, NamedRange, OptionGroup, PerGameCommonOptions, Range, Removed, Toggle
|
||||
|
||||
@@ -107,7 +107,7 @@ class ZillionStartChar(Choice):
|
||||
display_name = "start character"
|
||||
default = "random"
|
||||
|
||||
_name_capitalization: ClassVar[Dict[int, Chars]] = {
|
||||
_name_capitalization: ClassVar[dict[int, Chars]] = {
|
||||
option_jj: "JJ",
|
||||
option_apple: "Apple",
|
||||
option_champ: "Champ",
|
||||
@@ -263,7 +263,7 @@ class ZillionMapGen(Choice):
|
||||
option_full = 2
|
||||
default = 0
|
||||
|
||||
def zz_value(self) -> Literal['none', 'rooms', 'full']:
|
||||
def zz_value(self) -> Literal["none", "rooms", "full"]:
|
||||
if self.value == ZillionMapGen.option_none:
|
||||
return "none"
|
||||
if self.value == ZillionMapGen.option_rooms:
|
||||
@@ -305,7 +305,7 @@ z_option_groups = [
|
||||
]
|
||||
|
||||
|
||||
def convert_item_counts(ic: "Counter[str]") -> ZzItemCounts:
|
||||
def convert_item_counts(ic: Counter[str]) -> ZzItemCounts:
|
||||
tr: ZzItemCounts = {
|
||||
ID.card: ic["ID Card"],
|
||||
ID.red: ic["Red ID Card"],
|
||||
@@ -319,7 +319,7 @@ def convert_item_counts(ic: "Counter[str]") -> ZzItemCounts:
|
||||
return tr
|
||||
|
||||
|
||||
def validate(options: ZillionOptions) -> "Tuple[ZzOptions, Counter[str]]":
|
||||
def validate(options: ZillionOptions) -> tuple[ZzOptions, Counter[str]]:
|
||||
"""
|
||||
adjusts options to make game completion possible
|
||||
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import os
|
||||
from typing import Any, BinaryIO, Optional, cast
|
||||
from typing import BinaryIO
|
||||
import zipfile
|
||||
|
||||
from typing_extensions import override
|
||||
@@ -11,11 +11,11 @@ from zilliandomizer.patch import Patcher
|
||||
|
||||
from .gen_data import GenData
|
||||
|
||||
USHASH = 'd4bf9e7bcf9a48da53785d2ae7bc4270'
|
||||
US_HASH = "d4bf9e7bcf9a48da53785d2ae7bc4270"
|
||||
|
||||
|
||||
class ZillionPatch(APAutoPatchInterface):
|
||||
hash = USHASH
|
||||
hash = US_HASH
|
||||
game = "Zillion"
|
||||
patch_file_ending = ".apzl"
|
||||
result_file_ending = ".sms"
|
||||
@@ -23,8 +23,14 @@ class ZillionPatch(APAutoPatchInterface):
|
||||
gen_data_str: str
|
||||
""" JSON encoded """
|
||||
|
||||
def __init__(self, *args: Any, gen_data_str: str = "", **kwargs: Any) -> None:
|
||||
super().__init__(*args, **kwargs)
|
||||
def __init__(self,
|
||||
path: str | None = None,
|
||||
player: int | None = None,
|
||||
player_name: str = "",
|
||||
server: str = "",
|
||||
*,
|
||||
gen_data_str: str = "") -> None:
|
||||
super().__init__(path=path, player=player, player_name=player_name, server=server)
|
||||
self.gen_data_str = gen_data_str
|
||||
|
||||
@classmethod
|
||||
@@ -44,15 +50,17 @@ class ZillionPatch(APAutoPatchInterface):
|
||||
super().read_contents(opened_zipfile)
|
||||
self.gen_data_str = opened_zipfile.read("gen_data.json").decode()
|
||||
|
||||
@override
|
||||
def patch(self, target: str) -> None:
|
||||
self.read()
|
||||
write_rom_from_gen_data(self.gen_data_str, target)
|
||||
|
||||
|
||||
def get_base_rom_path(file_name: Optional[str] = None) -> str:
|
||||
options = Utils.get_options()
|
||||
def get_base_rom_path(file_name: str | None = None) -> str:
|
||||
from . import ZillionSettings, ZillionWorld
|
||||
settings: ZillionSettings = ZillionWorld.settings
|
||||
if not file_name:
|
||||
file_name = cast(str, options["zillion_options"]["rom_file"])
|
||||
file_name = settings.rom_file
|
||||
if not os.path.exists(file_name):
|
||||
file_name = Utils.user_path(file_name)
|
||||
return file_name
|
||||
|
||||
@@ -1,9 +1,11 @@
|
||||
from typing import Optional
|
||||
from BaseClasses import MultiWorld, Region, Location, Item, CollectionState
|
||||
from typing_extensions import override
|
||||
|
||||
from zilliandomizer.logic_components.regions import Region as ZzRegion
|
||||
from zilliandomizer.logic_components.locations import Location as ZzLocation
|
||||
from zilliandomizer.logic_components.items import RESCUE
|
||||
|
||||
from BaseClasses import MultiWorld, Region, Location, Item, CollectionState
|
||||
|
||||
from .id_maps import loc_name_to_id
|
||||
from .item import ZillionItem
|
||||
|
||||
@@ -28,12 +30,12 @@ class ZillionLocation(Location):
|
||||
zz_loc: ZzLocation,
|
||||
player: int,
|
||||
name: str,
|
||||
parent: Optional[Region] = None) -> None:
|
||||
parent: Region | None = None) -> None:
|
||||
loc_id = loc_name_to_id[name]
|
||||
super().__init__(player, name, loc_id, parent)
|
||||
self.zz_loc = zz_loc
|
||||
|
||||
# override
|
||||
@override
|
||||
def can_fill(self, state: CollectionState, item: Item, check_access: bool = True) -> bool:
|
||||
saved_gun_req = -1
|
||||
if isinstance(item, ZillionItem) \
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user