mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-03-29 13:13:24 -07:00
Merge branch 'main' into tunc-portal-direction-pairing
This commit is contained in:
1
.gitattributes
vendored
Normal file
1
.gitattributes
vendored
Normal file
@@ -0,0 +1 @@
|
||||
worlds/blasphemous/region_data.py linguist-generated=true
|
||||
130
BaseClasses.py
130
BaseClasses.py
@@ -11,8 +11,10 @@ from argparse import Namespace
|
||||
from collections import Counter, deque
|
||||
from collections.abc import Collection, MutableSequence
|
||||
from enum import IntEnum, IntFlag
|
||||
from typing import Any, Callable, Dict, Iterable, Iterator, List, Mapping, NamedTuple, Optional, Set, Tuple, \
|
||||
TypedDict, Union, Type, ClassVar
|
||||
from typing import (AbstractSet, Any, Callable, ClassVar, Dict, Iterable, Iterator, List, Mapping, NamedTuple,
|
||||
Optional, Protocol, Set, Tuple, Union, Type)
|
||||
|
||||
from typing_extensions import NotRequired, TypedDict
|
||||
|
||||
import NetUtils
|
||||
import Options
|
||||
@@ -22,16 +24,16 @@ if typing.TYPE_CHECKING:
|
||||
from worlds import AutoWorld
|
||||
|
||||
|
||||
class Group(TypedDict, total=False):
|
||||
class Group(TypedDict):
|
||||
name: str
|
||||
game: str
|
||||
world: "AutoWorld.World"
|
||||
players: Set[int]
|
||||
item_pool: Set[str]
|
||||
replacement_items: Dict[int, Optional[str]]
|
||||
local_items: Set[str]
|
||||
non_local_items: Set[str]
|
||||
link_replacement: bool
|
||||
players: AbstractSet[int]
|
||||
item_pool: NotRequired[Set[str]]
|
||||
replacement_items: NotRequired[Dict[int, Optional[str]]]
|
||||
local_items: NotRequired[Set[str]]
|
||||
non_local_items: NotRequired[Set[str]]
|
||||
link_replacement: NotRequired[bool]
|
||||
|
||||
|
||||
class ThreadBarrierProxy:
|
||||
@@ -48,6 +50,11 @@ class ThreadBarrierProxy:
|
||||
"Please use multiworld.per_slot_randoms[player] or randomize ahead of output.")
|
||||
|
||||
|
||||
class HasNameAndPlayer(Protocol):
|
||||
name: str
|
||||
player: int
|
||||
|
||||
|
||||
class MultiWorld():
|
||||
debug_types = False
|
||||
player_name: Dict[int, str]
|
||||
@@ -156,7 +163,7 @@ class MultiWorld():
|
||||
self.start_inventory_from_pool: Dict[int, Options.StartInventoryPool] = {}
|
||||
|
||||
for player in range(1, players + 1):
|
||||
def set_player_attr(attr, val):
|
||||
def set_player_attr(attr: str, val) -> None:
|
||||
self.__dict__.setdefault(attr, {})[player] = val
|
||||
set_player_attr('plando_items', [])
|
||||
set_player_attr('plando_texts', {})
|
||||
@@ -165,13 +172,13 @@ class MultiWorld():
|
||||
set_player_attr('completion_condition', lambda state: True)
|
||||
self.worlds = {}
|
||||
self.per_slot_randoms = Utils.DeprecateDict("Using per_slot_randoms is now deprecated. Please use the "
|
||||
"world's random object instead (usually self.random)")
|
||||
"world's random object instead (usually self.random)")
|
||||
self.plando_options = PlandoOptions.none
|
||||
|
||||
def get_all_ids(self) -> Tuple[int, ...]:
|
||||
return self.player_ids + tuple(self.groups)
|
||||
|
||||
def add_group(self, name: str, game: str, players: Set[int] = frozenset()) -> Tuple[int, Group]:
|
||||
def add_group(self, name: str, game: str, players: AbstractSet[int] = frozenset()) -> Tuple[int, Group]:
|
||||
"""Create a group with name and return the assigned player ID and group.
|
||||
If a group of this name already exists, the set of players is extended instead of creating a new one."""
|
||||
from worlds import AutoWorld
|
||||
@@ -195,7 +202,7 @@ class MultiWorld():
|
||||
|
||||
return new_id, new_group
|
||||
|
||||
def get_player_groups(self, player) -> Set[int]:
|
||||
def get_player_groups(self, player: int) -> Set[int]:
|
||||
return {group_id for group_id, group in self.groups.items() if player in group["players"]}
|
||||
|
||||
def set_seed(self, seed: Optional[int] = None, secure: bool = False, name: Optional[str] = None):
|
||||
@@ -258,7 +265,7 @@ class MultiWorld():
|
||||
"link_replacement": replacement_prio.index(item_link["link_replacement"]),
|
||||
}
|
||||
|
||||
for name, item_link in item_links.items():
|
||||
for _name, item_link in item_links.items():
|
||||
current_item_name_groups = AutoWorld.AutoWorldRegister.world_types[item_link["game"]].item_name_groups
|
||||
pool = set()
|
||||
local_items = set()
|
||||
@@ -388,7 +395,7 @@ class MultiWorld():
|
||||
return tuple(world for player, world in self.worlds.items() if
|
||||
player not in self.groups and self.game[player] == game_name)
|
||||
|
||||
def get_name_string_for_object(self, obj) -> str:
|
||||
def get_name_string_for_object(self, obj: HasNameAndPlayer) -> str:
|
||||
return obj.name if self.players == 1 else f'{obj.name} ({self.get_player_name(obj.player)})'
|
||||
|
||||
def get_player_name(self, player: int) -> str:
|
||||
@@ -430,7 +437,7 @@ class MultiWorld():
|
||||
subworld = self.worlds[player]
|
||||
for item in subworld.get_pre_fill_items():
|
||||
subworld.collect(ret, item)
|
||||
ret.sweep_for_events()
|
||||
ret.sweep_for_advancements()
|
||||
|
||||
if use_cache:
|
||||
self._all_state = ret
|
||||
@@ -439,7 +446,7 @@ class MultiWorld():
|
||||
def get_items(self) -> List[Item]:
|
||||
return [loc.item for loc in self.get_filled_locations()] + self.itempool
|
||||
|
||||
def find_item_locations(self, item, player: int, resolve_group_locations: bool = False) -> List[Location]:
|
||||
def find_item_locations(self, item: str, player: int, resolve_group_locations: bool = False) -> List[Location]:
|
||||
if resolve_group_locations:
|
||||
player_groups = self.get_player_groups(player)
|
||||
return [location for location in self.get_locations() if
|
||||
@@ -448,7 +455,7 @@ class MultiWorld():
|
||||
return [location for location in self.get_locations() if
|
||||
location.item and location.item.name == item and location.item.player == player]
|
||||
|
||||
def find_item(self, item, player: int) -> Location:
|
||||
def find_item(self, item: str, player: int) -> Location:
|
||||
return next(location for location in self.get_locations() if
|
||||
location.item and location.item.name == item and location.item.player == player)
|
||||
|
||||
@@ -541,9 +548,9 @@ class MultiWorld():
|
||||
return True
|
||||
state = starting_state.copy()
|
||||
else:
|
||||
if self.has_beaten_game(self.state):
|
||||
return True
|
||||
state = CollectionState(self)
|
||||
if self.has_beaten_game(state):
|
||||
return True
|
||||
prog_locations = {location for location in self.get_locations() if location.item
|
||||
and location.item.advancement and location not in state.locations_checked}
|
||||
|
||||
@@ -661,7 +668,7 @@ class CollectionState():
|
||||
multiworld: MultiWorld
|
||||
reachable_regions: Dict[int, Set[Region]]
|
||||
blocked_connections: Dict[int, Set[Entrance]]
|
||||
events: Set[Location]
|
||||
advancements: Set[Location]
|
||||
path: Dict[Union[Region, Entrance], PathValue]
|
||||
locations_checked: Set[Location]
|
||||
stale: Dict[int, bool]
|
||||
@@ -673,7 +680,7 @@ class CollectionState():
|
||||
self.multiworld = parent
|
||||
self.reachable_regions = {player: set() for player in parent.get_all_ids()}
|
||||
self.blocked_connections = {player: set() for player in parent.get_all_ids()}
|
||||
self.events = set()
|
||||
self.advancements = set()
|
||||
self.path = {}
|
||||
self.locations_checked = set()
|
||||
self.stale = {player: True for player in parent.get_all_ids()}
|
||||
@@ -722,7 +729,7 @@ class CollectionState():
|
||||
self.reachable_regions.items()}
|
||||
ret.blocked_connections = {player: entrance_set.copy() for player, entrance_set in
|
||||
self.blocked_connections.items()}
|
||||
ret.events = self.events.copy()
|
||||
ret.advancements = self.advancements.copy()
|
||||
ret.path = self.path.copy()
|
||||
ret.locations_checked = self.locations_checked.copy()
|
||||
for function in self.additional_copy_functions:
|
||||
@@ -755,19 +762,24 @@ class CollectionState():
|
||||
return self.multiworld.get_region(spot, player).can_reach(self)
|
||||
|
||||
def sweep_for_events(self, locations: Optional[Iterable[Location]] = None) -> None:
|
||||
Utils.deprecate("sweep_for_events has been renamed to sweep_for_advancements. The functionality is the same. "
|
||||
"Please switch over to sweep_for_advancements.")
|
||||
return self.sweep_for_advancements(locations)
|
||||
|
||||
def sweep_for_advancements(self, locations: Optional[Iterable[Location]] = None) -> None:
|
||||
if locations is None:
|
||||
locations = self.multiworld.get_filled_locations()
|
||||
reachable_events = True
|
||||
# since the loop has a good chance to run more than once, only filter the events once
|
||||
locations = {location for location in locations if location.advancement and location not in self.events}
|
||||
reachable_advancements = True
|
||||
# since the loop has a good chance to run more than once, only filter the advancements once
|
||||
locations = {location for location in locations if location.advancement and location not in self.advancements}
|
||||
|
||||
while reachable_events:
|
||||
reachable_events = {location for location in locations if location.can_reach(self)}
|
||||
locations -= reachable_events
|
||||
for event in reachable_events:
|
||||
self.events.add(event)
|
||||
assert isinstance(event.item, Item), "tried to collect Event with no Item"
|
||||
self.collect(event.item, True, event)
|
||||
while reachable_advancements:
|
||||
reachable_advancements = {location for location in locations if location.can_reach(self)}
|
||||
locations -= reachable_advancements
|
||||
for advancement in reachable_advancements:
|
||||
self.advancements.add(advancement)
|
||||
assert isinstance(advancement.item, Item), "tried to collect Event with no Item"
|
||||
self.collect(advancement.item, True, advancement)
|
||||
|
||||
# item name related
|
||||
def has(self, item: str, player: int, count: int = 1) -> bool:
|
||||
@@ -801,7 +813,7 @@ class CollectionState():
|
||||
if found >= count:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def has_from_list_unique(self, items: Iterable[str], player: int, count: int) -> bool:
|
||||
"""Returns True if the state contains at least `count` items matching any of the item names from a list.
|
||||
Ignores duplicates of the same item."""
|
||||
@@ -816,7 +828,7 @@ class CollectionState():
|
||||
def count_from_list(self, items: Iterable[str], player: int) -> int:
|
||||
"""Returns the cumulative count of items from a list present in state."""
|
||||
return sum(self.prog_items[player][item_name] for item_name in items)
|
||||
|
||||
|
||||
def count_from_list_unique(self, items: Iterable[str], player: int) -> int:
|
||||
"""Returns the cumulative count of items from a list present in state. Ignores duplicates of the same item."""
|
||||
return sum(self.prog_items[player][item_name] > 0 for item_name in items)
|
||||
@@ -871,7 +883,7 @@ class CollectionState():
|
||||
self.stale[item.player] = True
|
||||
|
||||
if changed and not prevent_sweep:
|
||||
self.sweep_for_events()
|
||||
self.sweep_for_advancements()
|
||||
|
||||
return changed
|
||||
|
||||
@@ -895,7 +907,7 @@ class Entrance:
|
||||
addresses = None
|
||||
target = None
|
||||
|
||||
def __init__(self, player: int, name: str = '', parent: Region = None):
|
||||
def __init__(self, player: int, name: str = "", parent: Optional[Region] = None) -> None:
|
||||
self.name = name
|
||||
self.parent_region = parent
|
||||
self.player = player
|
||||
@@ -915,9 +927,6 @@ class Entrance:
|
||||
region.entrances.append(self)
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
def __str__(self):
|
||||
multiworld = self.parent_region.multiworld if self.parent_region else None
|
||||
return multiworld.get_name_string_for_object(self) if multiworld else f'{self.name} (Player {self.player})'
|
||||
|
||||
@@ -1043,7 +1052,7 @@ class Region:
|
||||
self.locations.append(location_type(self.player, location, address, self))
|
||||
|
||||
def connect(self, connecting_region: Region, name: Optional[str] = None,
|
||||
rule: Optional[Callable[[CollectionState], bool]] = None) -> entrance_type:
|
||||
rule: Optional[Callable[[CollectionState], bool]] = None) -> Entrance:
|
||||
"""
|
||||
Connects this Region to another Region, placing the provided rule on the connection.
|
||||
|
||||
@@ -1083,9 +1092,6 @@ class Region:
|
||||
rules[connecting_region] if rules and connecting_region in rules else None)
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
def __str__(self):
|
||||
return self.multiworld.get_name_string_for_object(self) if self.multiworld else f'{self.name} (Player {self.player})'
|
||||
|
||||
|
||||
@@ -1104,9 +1110,9 @@ class Location:
|
||||
locked: bool = False
|
||||
show_in_spoiler: bool = True
|
||||
progress_type: LocationProgressType = LocationProgressType.DEFAULT
|
||||
always_allow = staticmethod(lambda state, item: False)
|
||||
always_allow: Callable[[CollectionState, Item], bool] = staticmethod(lambda state, item: False)
|
||||
access_rule: Callable[[CollectionState], bool] = staticmethod(lambda state: True)
|
||||
item_rule = staticmethod(lambda item: True)
|
||||
item_rule: Callable[[Item], bool] = staticmethod(lambda item: True)
|
||||
item: Optional[Item] = None
|
||||
|
||||
def __init__(self, player: int, name: str = '', address: Optional[int] = None, parent: Optional[Region] = None):
|
||||
@@ -1115,11 +1121,15 @@ class Location:
|
||||
self.address = address
|
||||
self.parent_region = parent
|
||||
|
||||
def can_fill(self, state: CollectionState, item: Item, check_access=True) -> bool:
|
||||
return ((self.always_allow(state, item) and item.name not in state.multiworld.worlds[item.player].options.non_local_items)
|
||||
or ((self.progress_type != LocationProgressType.EXCLUDED or not (item.advancement or item.useful))
|
||||
and self.item_rule(item)
|
||||
and (not check_access or self.can_reach(state))))
|
||||
def can_fill(self, state: CollectionState, item: Item, check_access: bool = True) -> bool:
|
||||
return ((
|
||||
self.always_allow(state, item)
|
||||
and item.name not in state.multiworld.worlds[item.player].options.non_local_items
|
||||
) or (
|
||||
(self.progress_type != LocationProgressType.EXCLUDED or not (item.advancement or item.useful))
|
||||
and self.item_rule(item)
|
||||
and (not check_access or self.can_reach(state))
|
||||
))
|
||||
|
||||
def can_reach(self, state: CollectionState) -> bool:
|
||||
# Region.can_reach is just a cache lookup, so placing it first for faster abort on average
|
||||
@@ -1134,9 +1144,6 @@ class Location:
|
||||
self.locked = True
|
||||
|
||||
def __repr__(self):
|
||||
return self.__str__()
|
||||
|
||||
def __str__(self):
|
||||
multiworld = self.parent_region.multiworld if self.parent_region and self.parent_region.multiworld else None
|
||||
return multiworld.get_name_string_for_object(self) if multiworld else f'{self.name} (Player {self.player})'
|
||||
|
||||
@@ -1158,7 +1165,7 @@ class Location:
|
||||
@property
|
||||
def native_item(self) -> bool:
|
||||
"""Returns True if the item in this location matches game."""
|
||||
return self.item and self.item.game == self.game
|
||||
return self.item is not None and self.item.game == self.game
|
||||
|
||||
@property
|
||||
def hint_text(self) -> str:
|
||||
@@ -1241,9 +1248,6 @@ class Item:
|
||||
return hash((self.name, self.player))
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return self.__str__()
|
||||
|
||||
def __str__(self) -> str:
|
||||
if self.location and self.location.parent_region and self.location.parent_region.multiworld:
|
||||
return self.location.parent_region.multiworld.get_name_string_for_object(self)
|
||||
return f"{self.name} (Player {self.player})"
|
||||
@@ -1321,9 +1325,9 @@ class Spoiler:
|
||||
|
||||
# in the second phase, we cull each sphere such that the game is still beatable,
|
||||
# reducing each range of influence to the bare minimum required inside it
|
||||
restore_later = {}
|
||||
restore_later: Dict[Location, Item] = {}
|
||||
for num, sphere in reversed(tuple(enumerate(collection_spheres))):
|
||||
to_delete = set()
|
||||
to_delete: Set[Location] = set()
|
||||
for location in sphere:
|
||||
# we remove the item at location and check if game is still beatable
|
||||
logging.debug('Checking if %s (Player %d) is required to beat the game.', location.item.name,
|
||||
@@ -1341,7 +1345,7 @@ class Spoiler:
|
||||
sphere -= to_delete
|
||||
|
||||
# second phase, sphere 0
|
||||
removed_precollected = []
|
||||
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)
|
||||
@@ -1494,9 +1498,9 @@ class Spoiler:
|
||||
|
||||
if self.paths:
|
||||
outfile.write('\n\nPaths:\n\n')
|
||||
path_listings = []
|
||||
path_listings: List[str] = []
|
||||
for location, path in sorted(self.paths.items()):
|
||||
path_lines = []
|
||||
path_lines: List[str] = []
|
||||
for region, exit in path:
|
||||
if exit is not None:
|
||||
path_lines.append("{} -> {}".format(region, exit))
|
||||
|
||||
@@ -252,7 +252,7 @@ class CommonContext:
|
||||
starting_reconnect_delay: int = 5
|
||||
current_reconnect_delay: int = starting_reconnect_delay
|
||||
command_processor: typing.Type[CommandProcessor] = ClientCommandProcessor
|
||||
ui = None
|
||||
ui: typing.Optional["kvui.GameManager"] = None
|
||||
ui_task: typing.Optional["asyncio.Task[None]"] = None
|
||||
input_task: typing.Optional["asyncio.Task[None]"] = None
|
||||
keep_alive_task: typing.Optional["asyncio.Task[None]"] = None
|
||||
|
||||
14
Fill.py
14
Fill.py
@@ -29,7 +29,7 @@ def sweep_from_pool(base_state: CollectionState, itempool: typing.Sequence[Item]
|
||||
new_state = base_state.copy()
|
||||
for item in itempool:
|
||||
new_state.collect(item, True)
|
||||
new_state.sweep_for_events(locations=locations)
|
||||
new_state.sweep_for_advancements(locations=locations)
|
||||
return new_state
|
||||
|
||||
|
||||
@@ -329,8 +329,8 @@ def accessibility_corrections(multiworld: MultiWorld, state: CollectionState, lo
|
||||
pool.append(location.item)
|
||||
state.remove(location.item)
|
||||
location.item = None
|
||||
if location in state.events:
|
||||
state.events.remove(location)
|
||||
if location in state.advancements:
|
||||
state.advancements.remove(location)
|
||||
locations.append(location)
|
||||
if pool and locations:
|
||||
locations.sort(key=lambda loc: loc.progress_type != LocationProgressType.PRIORITY)
|
||||
@@ -363,7 +363,7 @@ def distribute_early_items(multiworld: MultiWorld,
|
||||
early_priority_locations: typing.List[Location] = []
|
||||
loc_indexes_to_remove: typing.Set[int] = set()
|
||||
base_state = multiworld.state.copy()
|
||||
base_state.sweep_for_events(locations=(loc for loc in multiworld.get_filled_locations() if loc.address is None))
|
||||
base_state.sweep_for_advancements(locations=(loc for loc in multiworld.get_filled_locations() if loc.address is None))
|
||||
for i, loc in enumerate(fill_locations):
|
||||
if loc.can_reach(base_state):
|
||||
if loc.progress_type == LocationProgressType.PRIORITY:
|
||||
@@ -558,7 +558,7 @@ def flood_items(multiworld: MultiWorld) -> None:
|
||||
progress_done = False
|
||||
|
||||
# sweep once to pick up preplaced items
|
||||
multiworld.state.sweep_for_events()
|
||||
multiworld.state.sweep_for_advancements()
|
||||
|
||||
# fill multiworld from top of itempool while we can
|
||||
while not progress_done:
|
||||
@@ -746,7 +746,7 @@ def balance_multiworld_progression(multiworld: MultiWorld) -> None:
|
||||
), items_to_test):
|
||||
reducing_state.collect(location.item, True, location)
|
||||
|
||||
reducing_state.sweep_for_events(locations=locations_to_test)
|
||||
reducing_state.sweep_for_advancements(locations=locations_to_test)
|
||||
|
||||
if multiworld.has_beaten_game(balancing_state):
|
||||
if not multiworld.has_beaten_game(reducing_state):
|
||||
@@ -829,7 +829,7 @@ def distribute_planned(multiworld: MultiWorld) -> None:
|
||||
warn(warning, force)
|
||||
|
||||
swept_state = multiworld.state.copy()
|
||||
swept_state.sweep_for_events()
|
||||
swept_state.sweep_for_advancements()
|
||||
reachable = frozenset(multiworld.get_reachable_locations(swept_state))
|
||||
early_locations: typing.Dict[int, typing.List[str]] = collections.defaultdict(list)
|
||||
non_early_locations: typing.Dict[int, typing.List[str]] = collections.defaultdict(list)
|
||||
|
||||
@@ -511,7 +511,7 @@ def roll_settings(weights: dict, plando_options: PlandoOptions = PlandoOptions.b
|
||||
continue
|
||||
logging.warning(f"{option_key} is not a valid option name for {ret.game} and is not present in triggers.")
|
||||
if PlandoOptions.items in plando_options:
|
||||
ret.plando_items = game_weights.get("plando_items", [])
|
||||
ret.plando_items = copy.deepcopy(game_weights.get("plando_items", []))
|
||||
if ret.game == "A Link to the Past":
|
||||
roll_alttp_settings(ret, game_weights)
|
||||
|
||||
|
||||
9
KH1Client.py
Normal file
9
KH1Client.py
Normal file
@@ -0,0 +1,9 @@
|
||||
if __name__ == '__main__':
|
||||
import ModuleUpdate
|
||||
ModuleUpdate.update()
|
||||
|
||||
import Utils
|
||||
Utils.init_logging("KH1Client", exception_logger="Client")
|
||||
|
||||
from worlds.kh1.Client import launch
|
||||
launch()
|
||||
@@ -14,7 +14,7 @@ import tkinter as tk
|
||||
from argparse import Namespace
|
||||
from concurrent.futures import as_completed, ThreadPoolExecutor
|
||||
from glob import glob
|
||||
from tkinter import Tk, Frame, Label, StringVar, Entry, filedialog, messagebox, Button, Radiobutton, LEFT, X, TOP, LabelFrame, \
|
||||
from tkinter import Tk, Frame, Label, StringVar, Entry, filedialog, messagebox, Button, Radiobutton, LEFT, X, BOTH, TOP, LabelFrame, \
|
||||
IntVar, Checkbutton, E, W, OptionMenu, Toplevel, BOTTOM, RIGHT, font as font, PhotoImage
|
||||
from tkinter.constants import DISABLED, NORMAL
|
||||
from urllib.parse import urlparse
|
||||
@@ -29,7 +29,8 @@ from Utils import output_path, local_path, user_path, open_file, get_cert_none_s
|
||||
|
||||
|
||||
GAME_ALTTP = "A Link to the Past"
|
||||
|
||||
WINDOW_MIN_HEIGHT = 525
|
||||
WINDOW_MIN_WIDTH = 425
|
||||
|
||||
class AdjusterWorld(object):
|
||||
def __init__(self, sprite_pool):
|
||||
@@ -242,16 +243,17 @@ def adjustGUI():
|
||||
from argparse import Namespace
|
||||
from Utils import __version__ as MWVersion
|
||||
adjustWindow = Tk()
|
||||
adjustWindow.minsize(WINDOW_MIN_WIDTH, WINDOW_MIN_HEIGHT)
|
||||
adjustWindow.wm_title("Archipelago %s LttP Adjuster" % MWVersion)
|
||||
set_icon(adjustWindow)
|
||||
|
||||
rom_options_frame, rom_vars, set_sprite = get_rom_options_frame(adjustWindow)
|
||||
|
||||
bottomFrame2 = Frame(adjustWindow)
|
||||
bottomFrame2 = Frame(adjustWindow, padx=8, pady=2)
|
||||
|
||||
romFrame, romVar = get_rom_frame(adjustWindow)
|
||||
|
||||
romDialogFrame = Frame(adjustWindow)
|
||||
romDialogFrame = Frame(adjustWindow, padx=8, pady=2)
|
||||
baseRomLabel2 = Label(romDialogFrame, text='Rom to adjust')
|
||||
romVar2 = StringVar()
|
||||
romEntry2 = Entry(romDialogFrame, textvariable=romVar2)
|
||||
@@ -261,9 +263,9 @@ def adjustGUI():
|
||||
romVar2.set(rom)
|
||||
|
||||
romSelectButton2 = Button(romDialogFrame, text='Select Rom', command=RomSelect2)
|
||||
romDialogFrame.pack(side=TOP, expand=True, fill=X)
|
||||
baseRomLabel2.pack(side=LEFT)
|
||||
romEntry2.pack(side=LEFT, expand=True, fill=X)
|
||||
romDialogFrame.pack(side=TOP, expand=False, fill=X)
|
||||
baseRomLabel2.pack(side=LEFT, expand=False, fill=X, padx=(0, 8))
|
||||
romEntry2.pack(side=LEFT, expand=True, fill=BOTH, pady=1)
|
||||
romSelectButton2.pack(side=LEFT)
|
||||
|
||||
def adjustRom():
|
||||
@@ -331,12 +333,11 @@ def adjustGUI():
|
||||
messagebox.showinfo(title="Success", message="Settings saved to persistent storage")
|
||||
|
||||
adjustButton = Button(bottomFrame2, text='Adjust Rom', command=adjustRom)
|
||||
rom_options_frame.pack(side=TOP)
|
||||
rom_options_frame.pack(side=TOP, padx=8, pady=8, fill=BOTH, expand=True)
|
||||
adjustButton.pack(side=LEFT, padx=(5,5))
|
||||
|
||||
saveButton = Button(bottomFrame2, text='Save Settings', command=saveGUISettings)
|
||||
saveButton.pack(side=LEFT, padx=(5,5))
|
||||
|
||||
bottomFrame2.pack(side=TOP, pady=(5,5))
|
||||
|
||||
tkinter_center_window(adjustWindow)
|
||||
@@ -576,7 +577,7 @@ class AttachTooltip(object):
|
||||
def get_rom_frame(parent=None):
|
||||
adjuster_settings = get_adjuster_settings(GAME_ALTTP)
|
||||
|
||||
romFrame = Frame(parent)
|
||||
romFrame = Frame(parent, padx=8, pady=8)
|
||||
baseRomLabel = Label(romFrame, text='LttP Base Rom: ')
|
||||
romVar = StringVar(value=adjuster_settings.baserom)
|
||||
romEntry = Entry(romFrame, textvariable=romVar)
|
||||
@@ -596,20 +597,19 @@ def get_rom_frame(parent=None):
|
||||
romSelectButton = Button(romFrame, text='Select Rom', command=RomSelect)
|
||||
|
||||
baseRomLabel.pack(side=LEFT)
|
||||
romEntry.pack(side=LEFT, expand=True, fill=X)
|
||||
romEntry.pack(side=LEFT, expand=True, fill=BOTH, pady=1)
|
||||
romSelectButton.pack(side=LEFT)
|
||||
romFrame.pack(side=TOP, expand=True, fill=X)
|
||||
romFrame.pack(side=TOP, fill=X)
|
||||
|
||||
return romFrame, romVar
|
||||
|
||||
def get_rom_options_frame(parent=None):
|
||||
adjuster_settings = get_adjuster_settings(GAME_ALTTP)
|
||||
|
||||
romOptionsFrame = LabelFrame(parent, text="Rom options")
|
||||
romOptionsFrame.columnconfigure(0, weight=1)
|
||||
romOptionsFrame.columnconfigure(1, weight=1)
|
||||
romOptionsFrame = LabelFrame(parent, text="Rom options", padx=8, pady=8)
|
||||
|
||||
for i in range(5):
|
||||
romOptionsFrame.rowconfigure(i, weight=1)
|
||||
romOptionsFrame.rowconfigure(i, weight=0, pad=4)
|
||||
vars = Namespace()
|
||||
|
||||
vars.MusicVar = IntVar()
|
||||
@@ -660,7 +660,7 @@ def get_rom_options_frame(parent=None):
|
||||
spriteSelectButton = Button(spriteDialogFrame, text='...', command=SpriteSelect)
|
||||
|
||||
baseSpriteLabel.pack(side=LEFT)
|
||||
spriteEntry.pack(side=LEFT)
|
||||
spriteEntry.pack(side=LEFT, expand=True, fill=X)
|
||||
spriteSelectButton.pack(side=LEFT)
|
||||
|
||||
oofDialogFrame = Frame(romOptionsFrame)
|
||||
|
||||
@@ -79,6 +79,7 @@ class NetworkItem(typing.NamedTuple):
|
||||
item: int
|
||||
location: int
|
||||
player: int
|
||||
""" Sending player, except in LocationInfo (from LocationScouts), where it is the receiving player. """
|
||||
flags: int = 0
|
||||
|
||||
|
||||
|
||||
@@ -73,6 +73,9 @@ Currently, the following games are supported:
|
||||
* Yu-Gi-Oh! Ultimate Masters: World Championship Tournament 2006
|
||||
* A Hat in Time
|
||||
* Old School Runescape
|
||||
* Kingdom Hearts 1
|
||||
* Mega Man 2
|
||||
* Yacht Dice
|
||||
|
||||
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
|
||||
|
||||
@@ -78,6 +78,9 @@
|
||||
# Kirby's Dream Land 3
|
||||
/worlds/kdl3/ @Silvris
|
||||
|
||||
# Kingdom Hearts
|
||||
/worlds/kh1/ @gaithern
|
||||
|
||||
# Kingdom Hearts 2
|
||||
/worlds/kh2/ @JaredWeakStrike
|
||||
|
||||
@@ -103,6 +106,9 @@
|
||||
# Minecraft
|
||||
/worlds/minecraft/ @KonoTyran @espeon65536
|
||||
|
||||
# Mega Man 2
|
||||
/worlds/mm2/ @Silvris
|
||||
|
||||
# MegaMan Battle Network 3
|
||||
/worlds/mmbn3/ @digiholic
|
||||
|
||||
@@ -196,6 +202,9 @@
|
||||
# The Witness
|
||||
/worlds/witness/ @NewSoupVi @blastron
|
||||
|
||||
# Yacht Dice
|
||||
/worlds/yachtdice/ @spinerak
|
||||
|
||||
# Yoshi's Island
|
||||
/worlds/yoshisisland/ @PinkSwitch
|
||||
|
||||
|
||||
@@ -186,6 +186,11 @@ Root: HKCR; Subkey: "{#MyAppName}cv64patch"; ValueData: "Arc
|
||||
Root: HKCR; Subkey: "{#MyAppName}cv64patch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}cv64patch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".apmm2"; ValueData: "{#MyAppName}mm2patch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}mm2patch"; ValueData: "Archipelago Mega Man 2 Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}mm2patch\DefaultIcon"; ValueData: "{app}\ArchipelagoBizHawkClient.exe,0"; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}mm2patch\shell\open\command"; ValueData: """{app}\ArchipelagoBizHawkClient.exe"" ""%1"""; ValueType: string; ValueName: "";
|
||||
|
||||
Root: HKCR; Subkey: ".apladx"; ValueData: "{#MyAppName}ladxpatch"; Flags: uninsdeletevalue; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}ladxpatch"; ValueData: "Archipelago Links Awakening DX Patch"; Flags: uninsdeletekey; ValueType: string; ValueName: "";
|
||||
Root: HKCR; Subkey: "{#MyAppName}ladxpatch\DefaultIcon"; ValueData: "{app}\ArchipelagoLinksAwakeningClient.exe,0"; ValueType: string; ValueName: "";
|
||||
|
||||
2
kvui.py
2
kvui.py
@@ -5,6 +5,8 @@ import typing
|
||||
import re
|
||||
from collections import deque
|
||||
|
||||
assert "kivy" not in sys.modules, "kvui should be imported before kivy for frozen compatibility"
|
||||
|
||||
if sys.platform == "win32":
|
||||
import ctypes
|
||||
|
||||
|
||||
@@ -24,7 +24,7 @@ class TestBase(unittest.TestCase):
|
||||
for item in items:
|
||||
item.classification = ItemClassification.progression
|
||||
state.collect(item, prevent_sweep=True)
|
||||
state.sweep_for_events()
|
||||
state.sweep_for_advancements()
|
||||
state.update_reachable_regions(1)
|
||||
self._state_cache[self.multiworld, tuple(items)] = state
|
||||
return state
|
||||
@@ -221,8 +221,8 @@ class WorldTestBase(unittest.TestCase):
|
||||
if isinstance(items, Item):
|
||||
items = (items,)
|
||||
for item in items:
|
||||
if item.location and item.advancement and item.location in self.multiworld.state.events:
|
||||
self.multiworld.state.events.remove(item.location)
|
||||
if item.location and item.advancement and item.location in self.multiworld.state.advancements:
|
||||
self.multiworld.state.advancements.remove(item.location)
|
||||
self.multiworld.state.remove(item)
|
||||
|
||||
def can_reach_location(self, location: str) -> bool:
|
||||
|
||||
@@ -192,7 +192,7 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
location_pool = player1.locations[1:] + player2.locations
|
||||
item_pool = player1.prog_items[:-1] + player2.prog_items
|
||||
fill_restrictive(multiworld, multiworld.state, location_pool, item_pool)
|
||||
multiworld.state.sweep_for_events() # collect everything
|
||||
multiworld.state.sweep_for_advancements() # collect everything
|
||||
|
||||
# all of player2's locations and items should be accessible (not all of player1's)
|
||||
for item in player2.prog_items:
|
||||
@@ -443,8 +443,8 @@ class TestFillRestrictive(unittest.TestCase):
|
||||
item = player1.prog_items[0]
|
||||
item.code = None
|
||||
location.place_locked_item(item)
|
||||
multiworld.state.sweep_for_events()
|
||||
multiworld.state.sweep_for_events()
|
||||
multiworld.state.sweep_for_advancements()
|
||||
multiworld.state.sweep_for_advancements()
|
||||
self.assertTrue(multiworld.state.prog_items[item.player][item.name], "Sweep did not collect - Test flawed")
|
||||
self.assertEqual(multiworld.state.prog_items[item.player][item.name], 1, "Sweep collected multiple times")
|
||||
|
||||
|
||||
@@ -14,6 +14,18 @@ class TestBase(unittest.TestCase):
|
||||
"Desert Northern Cliffs", # on top of mountain, only reachable via OWG
|
||||
"Dark Death Mountain Bunny Descent Area" # OWG Mountain descent
|
||||
},
|
||||
# These Blasphemous regions are not reachable with default options
|
||||
"Blasphemous": {
|
||||
"D01Z04S13[SE]", # difficulty must be hard
|
||||
"D01Z05S25[E]", # difficulty must be hard
|
||||
"D02Z02S05[W]", # difficulty must be hard and purified_hand must be true
|
||||
"D04Z01S06[E]", # purified_hand must be true
|
||||
"D04Z02S02[NE]", # difficulty must be hard and purified_hand must be true
|
||||
"D05Z01S11[SW]", # difficulty must be hard
|
||||
"D06Z01S08[N]", # difficulty must be hard and purified_hand must be true
|
||||
"D20Z02S11[NW]", # difficulty must be hard
|
||||
"D20Z02S11[E]", # difficulty must be hard
|
||||
},
|
||||
"Ocarina of Time": {
|
||||
"Prelude of Light Warp", # Prelude is not progression by default
|
||||
"Serenade of Water Warp", # Serenade is not progression by default
|
||||
|
||||
@@ -132,7 +132,8 @@ def _install_apworld(apworld_src: str = "") -> Optional[Tuple[pathlib.Path, path
|
||||
break
|
||||
if found_already_loaded:
|
||||
raise Exception(f"Installed APWorld successfully, but '{module_name}' is already loaded,\n"
|
||||
"so a Launcher restart is required to use the new installation.")
|
||||
"so a Launcher restart is required to use the new installation.\n"
|
||||
"If the Launcher is not open, no action needs to be taken.")
|
||||
world_source = worlds.WorldSource(str(target), is_zip=True)
|
||||
bisect.insort(worlds.world_sources, world_source)
|
||||
world_source.load()
|
||||
|
||||
@@ -381,8 +381,8 @@ def set_moderate_rules(world: "HatInTimeWorld"):
|
||||
lambda state: can_use_hat(state, world, HatType.ICE), "or")
|
||||
|
||||
# Moderate: Clock Tower Chest + Ruined Tower with nothing
|
||||
add_rule(world.multiworld.get_location("Mafia Town - Clock Tower Chest", world.player), lambda state: True)
|
||||
add_rule(world.multiworld.get_location("Mafia Town - Top of Ruined Tower", world.player), lambda state: True)
|
||||
set_rule(world.multiworld.get_location("Mafia Town - Clock Tower Chest", world.player), lambda state: True)
|
||||
set_rule(world.multiworld.get_location("Mafia Town - Top of Ruined Tower", world.player), lambda state: True)
|
||||
|
||||
# Moderate: enter and clear The Subcon Well without Hookshot and without hitting the bell
|
||||
for loc in world.multiworld.get_region("The Subcon Well", world.player).locations:
|
||||
@@ -432,8 +432,8 @@ def set_moderate_rules(world: "HatInTimeWorld"):
|
||||
|
||||
if world.is_dlc1():
|
||||
# Moderate: clear Rock the Boat without Ice Hat
|
||||
add_rule(world.multiworld.get_location("Rock the Boat - Post Captain Rescue", world.player), lambda state: True)
|
||||
add_rule(world.multiworld.get_location("Act Completion (Rock the Boat)", world.player), lambda state: True)
|
||||
set_rule(world.multiworld.get_location("Rock the Boat - Post Captain Rescue", world.player), lambda state: True)
|
||||
set_rule(world.multiworld.get_location("Act Completion (Rock the Boat)", world.player), lambda state: True)
|
||||
|
||||
# Moderate: clear Deep Sea without Ice Hat
|
||||
set_rule(world.multiworld.get_location("Act Completion (Time Rift - Deep Sea)", world.player),
|
||||
@@ -855,6 +855,9 @@ def set_rift_rules(world: "HatInTimeWorld", regions: Dict[str, Region]):
|
||||
|
||||
for entrance in regions["Time Rift - Alpine Skyline"].entrances:
|
||||
add_rule(entrance, lambda state: has_relic_combo(state, world, "Crayon"))
|
||||
if entrance.parent_region.name == "Alpine Free Roam":
|
||||
add_rule(entrance,
|
||||
lambda state: can_use_hookshot(state, world) and can_hit(state, world, umbrella_only=True))
|
||||
|
||||
if world.is_dlc1():
|
||||
for entrance in regions["Time Rift - Balcony"].entrances:
|
||||
@@ -933,6 +936,9 @@ def set_default_rift_rules(world: "HatInTimeWorld"):
|
||||
|
||||
for entrance in world.multiworld.get_region("Time Rift - Alpine Skyline", world.player).entrances:
|
||||
add_rule(entrance, lambda state: has_relic_combo(state, world, "Crayon"))
|
||||
if entrance.parent_region.name == "Alpine Free Roam":
|
||||
add_rule(entrance,
|
||||
lambda state: can_use_hookshot(state, world) and can_hit(state, world, umbrella_only=True))
|
||||
|
||||
if world.is_dlc1():
|
||||
for entrance in world.multiworld.get_region("Time Rift - Balcony", world.player).entrances:
|
||||
|
||||
@@ -248,7 +248,7 @@ def fill_dungeons_restrictive(multiworld: MultiWorld):
|
||||
pass
|
||||
for item in pre_fill_items:
|
||||
multiworld.worlds[item.player].collect(all_state_base, item)
|
||||
all_state_base.sweep_for_events()
|
||||
all_state_base.sweep_for_advancements()
|
||||
|
||||
# Remove completion condition so that minimal-accessibility worlds place keys properly
|
||||
for player in {item.player for item in in_dungeon_items}:
|
||||
@@ -262,8 +262,8 @@ def fill_dungeons_restrictive(multiworld: MultiWorld):
|
||||
all_state_base.remove(item_factory(key_data[3], multiworld.worlds[player]))
|
||||
loc = multiworld.get_location(key_loc, player)
|
||||
|
||||
if loc in all_state_base.events:
|
||||
all_state_base.events.remove(loc)
|
||||
if loc in all_state_base.advancements:
|
||||
all_state_base.advancements.remove(loc)
|
||||
fill_restrictive(multiworld, all_state_base, locations, in_dungeon_items, lock=True, allow_excluded=True,
|
||||
name="LttP Dungeon Items")
|
||||
|
||||
|
||||
@@ -682,7 +682,7 @@ def get_pool_core(world, player: int):
|
||||
if 'triforce_hunt' in goal:
|
||||
|
||||
if world.triforce_pieces_mode[player].value == TriforcePiecesMode.option_extra:
|
||||
treasure_hunt_total = (world.triforce_pieces_available[player].value
|
||||
treasure_hunt_total = (world.triforce_pieces_required[player].value
|
||||
+ world.triforce_pieces_extra[player].value)
|
||||
elif world.triforce_pieces_mode[player].value == TriforcePiecesMode.option_percentage:
|
||||
percentage = float(world.triforce_pieces_percentage[player].value) / 100
|
||||
|
||||
@@ -412,7 +412,7 @@ def global_rules(multiworld: MultiWorld, player: int):
|
||||
lambda state: ((state._lttp_has_key('Small Key (Thieves Town)', player, 3)) or (location_item_name(state, 'Thieves\' Town - Big Chest', player) == ("Small Key (Thieves Town)", player)) and state._lttp_has_key('Small Key (Thieves Town)', player, 2)) and state.has('Hammer', player))
|
||||
set_rule(multiworld.get_location('Thieves\' Town - Blind\'s Cell', player),
|
||||
lambda state: state._lttp_has_key('Small Key (Thieves Town)', player))
|
||||
if multiworld.accessibility[player] != 'locations' and not multiworld.key_drop_shuffle[player]:
|
||||
if multiworld.accessibility[player] != 'full' and not multiworld.key_drop_shuffle[player]:
|
||||
set_always_allow(multiworld.get_location('Thieves\' Town - Big Chest', player), lambda state, item: item.name == 'Small Key (Thieves Town)' and item.player == player)
|
||||
set_rule(multiworld.get_location('Thieves\' Town - Attic', player), lambda state: state._lttp_has_key('Small Key (Thieves Town)', player, 3))
|
||||
set_rule(multiworld.get_location('Thieves\' Town - Spike Switch Pot Key', player),
|
||||
@@ -547,7 +547,7 @@ def global_rules(multiworld: MultiWorld, player: int):
|
||||
location_item_name(state, 'Ganons Tower - Map Chest', player) in [('Big Key (Ganons Tower)', player)] and state._lttp_has_key('Small Key (Ganons Tower)', player, 6)))
|
||||
|
||||
# this seemed to be causing generation failure, disable for now
|
||||
# if world.accessibility[player] != 'locations':
|
||||
# if world.accessibility[player] != 'full':
|
||||
# set_always_allow(world.get_location('Ganons Tower - Map Chest', player), lambda state, item: item.name == 'Small Key (Ganons Tower)' and item.player == player and state._lttp_has_key('Small Key (Ganons Tower)', player, 7) and state.can_reach('Ganons Tower (Hookshot Room)', 'region', player))
|
||||
|
||||
# It is possible to need more than 6 keys to get through this entrance if you spend keys elsewhere. We reflect this in the chest requirements.
|
||||
|
||||
@@ -356,6 +356,8 @@ class ALTTPWorld(World):
|
||||
self.dungeon_local_item_names |= self.item_name_groups[option.item_name_group]
|
||||
if option == "original_dungeon":
|
||||
self.dungeon_specific_item_names |= self.item_name_groups[option.item_name_group]
|
||||
else:
|
||||
self.options.local_items.value |= self.dungeon_local_item_names
|
||||
|
||||
self.difficulty_requirements = difficulties[multiworld.item_pool[player].current_key]
|
||||
|
||||
|
||||
@@ -54,7 +54,7 @@ class TestDungeon(LTTPTestBase):
|
||||
|
||||
for item in items:
|
||||
item.classification = ItemClassification.progression
|
||||
state.collect(item, prevent_sweep=True) # prevent_sweep=True prevents running sweep_for_events() and picking up
|
||||
state.sweep_for_events() # key drop keys repeatedly
|
||||
state.collect(item, prevent_sweep=True) # prevent_sweep=True prevents running sweep_for_advancements() and picking up
|
||||
state.sweep_for_advancements() # key drop keys repeatedly
|
||||
|
||||
self.assertEqual(self.multiworld.get_location(location, 1).can_reach(state), access, f"failed {self.multiworld.get_location(location, 1)} with: {item_pool}")
|
||||
|
||||
60
worlds/alttp/test/options/test_dungeon_fill.py
Normal file
60
worlds/alttp/test/options/test_dungeon_fill.py
Normal file
@@ -0,0 +1,60 @@
|
||||
from unittest import TestCase
|
||||
|
||||
from BaseClasses import MultiWorld
|
||||
from test.general import gen_steps, setup_multiworld
|
||||
from worlds.AutoWorld import call_all
|
||||
from worlds.generic.Rules import locality_rules
|
||||
from ... import ALTTPWorld
|
||||
from ...Options import DungeonItem
|
||||
|
||||
|
||||
class DungeonFillTestBase(TestCase):
|
||||
multiworld: MultiWorld
|
||||
world_1: ALTTPWorld
|
||||
world_2: ALTTPWorld
|
||||
options = (
|
||||
"big_key_shuffle",
|
||||
"small_key_shuffle",
|
||||
"key_drop_shuffle",
|
||||
"compass_shuffle",
|
||||
"map_shuffle",
|
||||
)
|
||||
|
||||
def setUp(self):
|
||||
self.multiworld = setup_multiworld([ALTTPWorld, ALTTPWorld], ())
|
||||
self.world_1 = self.multiworld.worlds[1]
|
||||
self.world_2 = self.multiworld.worlds[2]
|
||||
|
||||
def generate_with_options(self, option_value: int):
|
||||
for option in self.options:
|
||||
getattr(self.world_1.options, option).value = getattr(self.world_2.options, option).value = option_value
|
||||
|
||||
for step in gen_steps:
|
||||
call_all(self.multiworld, step)
|
||||
# this is where locality rules are set in normal generation which we need to verify this test
|
||||
if step == "set_rules":
|
||||
locality_rules(self.multiworld)
|
||||
|
||||
def test_original_dungeons(self):
|
||||
self.generate_with_options(DungeonItem.option_original_dungeon)
|
||||
for location in self.multiworld.get_filled_locations():
|
||||
with (self.subTest(location=location)):
|
||||
if location.parent_region.dungeon is None:
|
||||
self.assertIs(location.item.dungeon, None)
|
||||
else:
|
||||
self.assertEqual(location.player, location.item.player,
|
||||
f"{location.item} does not belong to {location}'s player")
|
||||
if location.item.dungeon is None:
|
||||
continue
|
||||
self.assertIs(location.item.dungeon, location.parent_region.dungeon,
|
||||
f"{location.item} was not placed in its original dungeon.")
|
||||
|
||||
def test_own_dungeons(self):
|
||||
self.generate_with_options(DungeonItem.option_own_dungeons)
|
||||
for location in self.multiworld.get_filled_locations():
|
||||
with self.subTest(location=location):
|
||||
if location.parent_region.dungeon is None:
|
||||
self.assertIs(location.item.dungeon, None)
|
||||
else:
|
||||
self.assertEqual(location.player, location.item.player,
|
||||
f"{location.item} does not belong to {location}'s player")
|
||||
@@ -4,7 +4,7 @@ from BaseClasses import Tutorial
|
||||
from ..AutoWorld import WebWorld, World
|
||||
|
||||
class AP_SudokuWebWorld(WebWorld):
|
||||
options_page = "games/Sudoku/info/en"
|
||||
options_page = False
|
||||
theme = 'partyTime'
|
||||
|
||||
setup_en = Tutorial(
|
||||
|
||||
@@ -1,9 +1,7 @@
|
||||
# APSudoku Setup Guide
|
||||
|
||||
## Required Software
|
||||
- [APSudoku](https://github.com/EmilyV99/APSudoku)
|
||||
- Windows (most tested on Win10)
|
||||
- Other platforms might be able to build from source themselves; and may be included in the future.
|
||||
- [APSudoku](https://github.com/APSudoku/APSudoku)
|
||||
|
||||
## General Concept
|
||||
|
||||
@@ -13,25 +11,33 @@ Does not need to be added at the start of a seed, as it does not create any slot
|
||||
|
||||
## Installation Procedures
|
||||
|
||||
Go to the latest release from the [APSudoku Releases page](https://github.com/EmilyV99/APSudoku/releases). Download and extract the `APSudoku.zip` file.
|
||||
Go to the latest release from the [APSudoku Releases page](https://github.com/APSudoku/APSudoku/releases/latest). Download and extract the appropriate file for your platform.
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
1. Run APSudoku.exe
|
||||
2. Under the 'Archipelago' tab at the top-right:
|
||||
- Enter the server url & port number
|
||||
1. Run the APSudoku executable.
|
||||
2. Under `Settings` → `Connection` at the top-right:
|
||||
- Enter the server address and port number
|
||||
- Enter the name of the slot you wish to connect to
|
||||
- Enter the room password (optional)
|
||||
- Select DeathLink related settings (optional)
|
||||
- Press connect
|
||||
3. Go back to the 'Sudoku' tab
|
||||
- Click the various '?' buttons for information on how to play / control
|
||||
4. Choose puzzle difficulty
|
||||
5. Try to solve the Sudoku. Click 'Check' when done.
|
||||
- Press `Connect`
|
||||
4. Under the `Sudoku` tab
|
||||
- Choose puzzle difficulty
|
||||
- Click `Start` to generate a puzzle
|
||||
5. Try to solve the Sudoku. Click `Check` when done
|
||||
- A correct solution rewards you with 1 hint for a location in the world you are connected to
|
||||
- An incorrect solution has no penalty, unless DeathLink is enabled (see below)
|
||||
|
||||
Info:
|
||||
- You can set various settings under `Settings` → `Sudoku`, and can change the colors used under `Settings` → `Theme`.
|
||||
- While connected, you can view the `Console` and `Hints` tabs for standard TextClient-like features
|
||||
- You can also use the `Tracking` tab to view either a basic tracker or a valid [GodotAP tracker pack](https://github.com/EmilyV99/GodotAP/blob/main/tracker_packs/GET_PACKS.md)
|
||||
- While connected, the number of "unhinted" locations for your slot is shown in the upper-left of the the `Sudoku` tab. (If this reads 0, no further hints can be earned for this slot, as every locations is already hinted)
|
||||
- Click the various `?` buttons for information on controls/how to play
|
||||
## DeathLink Support
|
||||
|
||||
If 'DeathLink' is enabled when you click 'Connect':
|
||||
- Lose a life if you check an incorrect puzzle (not an _incomplete_ puzzle- if any cells are empty, you get off with a warning), or quit a puzzle without solving it (including disconnecting).
|
||||
- Life count customizable (default 0). Dying with 0 lives left kills linked players AND resets your puzzle.
|
||||
If `DeathLink` is enabled when you click `Connect`:
|
||||
- Lose a life if you check an incorrect puzzle (not an _incomplete_ puzzle- if any cells are empty, you get off with a warning), or if you quit a puzzle without solving it (including disconnecting).
|
||||
- Your life count is customizable (default 0). Dying with 0 lives left kills linked players AND resets your puzzle.
|
||||
- On receiving a DeathLink from another player, your puzzle resets.
|
||||
|
||||
19
worlds/blasphemous/ExtractorConfig.json
Normal file
19
worlds/blasphemous/ExtractorConfig.json
Normal file
@@ -0,0 +1,19 @@
|
||||
{
|
||||
"type": "WorldDefinition",
|
||||
"configuration": "./output/StringWorldDefinition.json",
|
||||
"emptyRegionsToKeep": [
|
||||
"D17Z01S01",
|
||||
"D01Z02S01",
|
||||
"D02Z03S09",
|
||||
"D03Z03S11",
|
||||
"D04Z03S01",
|
||||
"D06Z01S09",
|
||||
"D20Z02S09",
|
||||
"D09Z01S09[Cell24]",
|
||||
"D09Z01S08[Cell7]",
|
||||
"D09Z01S08[Cell18]",
|
||||
"D09BZ01S01[Cell24]",
|
||||
"D09BZ01S01[Cell17]",
|
||||
"D09BZ01S01[Cell19]"
|
||||
]
|
||||
}
|
||||
@@ -637,52 +637,35 @@ item_table: List[ItemDict] = [
|
||||
'classification': ItemClassification.filler}
|
||||
]
|
||||
|
||||
event_table: Dict[str, str] = {
|
||||
"OpenedDCGateW": "D01Z05S24",
|
||||
"OpenedDCGateE": "D01Z05S12",
|
||||
"OpenedDCLadder": "D01Z05S20",
|
||||
"OpenedWOTWCave": "D02Z01S06",
|
||||
"RodeGOTPElevator": "D02Z02S11",
|
||||
"OpenedConventLadder": "D02Z03S11",
|
||||
"BrokeJondoBellW": "D03Z02S09",
|
||||
"BrokeJondoBellE": "D03Z02S05",
|
||||
"OpenedMOMLadder": "D04Z02S06",
|
||||
"OpenedTSCGate": "D05Z02S11",
|
||||
"OpenedARLadder": "D06Z01S23",
|
||||
"BrokeBOTTCStatue": "D08Z01S02",
|
||||
"OpenedWOTHPGate": "D09Z01S05",
|
||||
"OpenedBOTSSLadder": "D17Z01S04"
|
||||
}
|
||||
|
||||
group_table: Dict[str, Set[str]] = {
|
||||
"wounds" : ["Holy Wound of Attrition",
|
||||
"wounds" : {"Holy Wound of Attrition",
|
||||
"Holy Wound of Contrition",
|
||||
"Holy Wound of Compunction"],
|
||||
"Holy Wound of Compunction"},
|
||||
|
||||
"masks" : ["Deformed Mask of Orestes",
|
||||
"masks" : {"Deformed Mask of Orestes",
|
||||
"Mirrored Mask of Dolphos",
|
||||
"Embossed Mask of Crescente"],
|
||||
"Embossed Mask of Crescente"},
|
||||
|
||||
"marks" : ["Mark of the First Refuge",
|
||||
"marks" : {"Mark of the First Refuge",
|
||||
"Mark of the Second Refuge",
|
||||
"Mark of the Third Refuge"],
|
||||
"Mark of the Third Refuge"},
|
||||
|
||||
"tirso" : ["Bouquet of Rosemary",
|
||||
"tirso" : {"Bouquet of Rosemary",
|
||||
"Incense Garlic",
|
||||
"Olive Seeds",
|
||||
"Dried Clove",
|
||||
"Sooty Garlic",
|
||||
"Bouquet of Thyme"],
|
||||
"Bouquet of Thyme"},
|
||||
|
||||
"tentudia": ["Tentudia's Carnal Remains",
|
||||
"tentudia": {"Tentudia's Carnal Remains",
|
||||
"Remains of Tentudia's Hair",
|
||||
"Tentudia's Skeletal Remains"],
|
||||
"Tentudia's Skeletal Remains"},
|
||||
|
||||
"egg" : ["Melted Golden Coins",
|
||||
"egg" : {"Melted Golden Coins",
|
||||
"Torn Bridal Ribbon",
|
||||
"Black Grieving Veil"],
|
||||
"Black Grieving Veil"},
|
||||
|
||||
"bones" : ["Parietal bone of Lasser, the Inquisitor",
|
||||
"bones" : {"Parietal bone of Lasser, the Inquisitor",
|
||||
"Jaw of Ashgan, the Inquisitor",
|
||||
"Cervical vertebra of Zicher, the Brewmaster",
|
||||
"Clavicle of Dalhuisen, the Schoolchild",
|
||||
@@ -725,14 +708,14 @@ group_table: Dict[str, Set[str]] = {
|
||||
"Scaphoid of Fierce, the Leper",
|
||||
"Anklebone of Weston, the Pilgrim",
|
||||
"Calcaneum of Persian, the Bandit",
|
||||
"Navicular of Kahnnyhoo, the Murderer"],
|
||||
"Navicular of Kahnnyhoo, the Murderer"},
|
||||
|
||||
"power" : ["Life Upgrade",
|
||||
"power" : {"Life Upgrade",
|
||||
"Fervour Upgrade",
|
||||
"Empty Bile Vessel",
|
||||
"Quicksilver"],
|
||||
"Quicksilver"},
|
||||
|
||||
"prayer" : ["Seguiriya to your Eyes like Stars",
|
||||
"prayer" : {"Seguiriya to your Eyes like Stars",
|
||||
"Debla of the Lights",
|
||||
"Saeta Dolorosa",
|
||||
"Campanillero to the Sons of the Aurora",
|
||||
@@ -746,10 +729,17 @@ group_table: Dict[str, Set[str]] = {
|
||||
"Romance to the Crimson Mist",
|
||||
"Zambra to the Resplendent Crown",
|
||||
"Cantina of the Blue Rose",
|
||||
"Mirabras of the Return to Port"]
|
||||
"Mirabras of the Return to Port"},
|
||||
|
||||
"toe" : {"Little Toe made of Limestone",
|
||||
"Big Toe made of Limestone",
|
||||
"Fourth Toe made of Limestone"},
|
||||
|
||||
"eye" : {"Severed Right Eye of the Traitor",
|
||||
"Broken Left Eye of the Traitor"}
|
||||
}
|
||||
|
||||
tears_set: Set[str] = [
|
||||
tears_list: List[str] = [
|
||||
"Tears of Atonement (500)",
|
||||
"Tears of Atonement (625)",
|
||||
"Tears of Atonement (750)",
|
||||
@@ -772,16 +762,16 @@ tears_set: Set[str] = [
|
||||
"Tears of Atonement (30000)"
|
||||
]
|
||||
|
||||
reliquary_set: Set[str] = [
|
||||
reliquary_set: Set[str] = {
|
||||
"Reliquary of the Fervent Heart",
|
||||
"Reliquary of the Suffering Heart",
|
||||
"Reliquary of the Sorrowful Heart"
|
||||
]
|
||||
}
|
||||
|
||||
skill_set: Set[str] = [
|
||||
skill_set: Set[str] = {
|
||||
"Combo Skill",
|
||||
"Charged Skill",
|
||||
"Ranged Skill",
|
||||
"Dive Skill",
|
||||
"Lunge Skill"
|
||||
]
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,4 +1,5 @@
|
||||
from Options import Choice, Toggle, DefaultOnToggle, DeathLink, StartInventoryPool
|
||||
from dataclasses import dataclass
|
||||
from Options import Choice, Toggle, DefaultOnToggle, DeathLink, PerGameCommonOptions, OptionGroup
|
||||
import random
|
||||
|
||||
|
||||
@@ -20,23 +21,30 @@ class ChoiceIsRandom(Choice):
|
||||
|
||||
|
||||
class PrieDieuWarp(DefaultOnToggle):
|
||||
"""Automatically unlocks the ability to warp between Prie Dieu shrines."""
|
||||
"""
|
||||
Automatically unlocks the ability to warp between Prie Dieu shrines.
|
||||
"""
|
||||
display_name = "Unlock Fast Travel"
|
||||
|
||||
|
||||
class SkipCutscenes(DefaultOnToggle):
|
||||
"""Automatically skips most cutscenes."""
|
||||
"""
|
||||
Automatically skips most cutscenes.
|
||||
"""
|
||||
display_name = "Auto Skip Cutscenes"
|
||||
|
||||
|
||||
class CorpseHints(DefaultOnToggle):
|
||||
"""Changes the 34 corpses in game to give various hints about item locations."""
|
||||
"""
|
||||
Changes the 34 corpses in game to give various hints about item locations.
|
||||
"""
|
||||
display_name = "Corpse Hints"
|
||||
|
||||
|
||||
class Difficulty(Choice):
|
||||
"""Adjusts the overall difficulty of the randomizer, including upgrades required to defeat bosses
|
||||
and advanced movement tricks or glitches."""
|
||||
"""
|
||||
Adjusts the overall difficulty of the randomizer, including upgrades required to defeat bosses and advanced movement tricks or glitches.
|
||||
"""
|
||||
display_name = "Difficulty"
|
||||
option_easy = 0
|
||||
option_normal = 1
|
||||
@@ -45,15 +53,18 @@ class Difficulty(Choice):
|
||||
|
||||
|
||||
class Penitence(Toggle):
|
||||
"""Allows one of the three Penitences to be chosen at the beginning of the game."""
|
||||
"""
|
||||
Allows one of the three Penitences to be chosen at the beginning of the game.
|
||||
"""
|
||||
display_name = "Penitence"
|
||||
|
||||
|
||||
class StartingLocation(ChoiceIsRandom):
|
||||
"""Choose where to start the randomizer. Note that some starting locations cannot be chosen with certain
|
||||
other options.
|
||||
Specifically, Brotherhood and Mourning And Havoc cannot be chosen if Shuffle Dash is enabled, and Grievance Ascends
|
||||
cannot be chosen if Shuffle Wall Climb is enabled."""
|
||||
"""
|
||||
Choose where to start the randomizer. Note that some starting locations cannot be chosen with certain other options.
|
||||
|
||||
Specifically, Brotherhood and Mourning And Havoc cannot be chosen if Shuffle Dash is enabled, and Grievance Ascends cannot be chosen if Shuffle Wall Climb is enabled.
|
||||
"""
|
||||
display_name = "Starting Location"
|
||||
option_brotherhood = 0
|
||||
option_albero = 1
|
||||
@@ -66,10 +77,15 @@ class StartingLocation(ChoiceIsRandom):
|
||||
|
||||
|
||||
class Ending(Choice):
|
||||
"""Choose which ending is required to complete the game.
|
||||
"""
|
||||
Choose which ending is required to complete the game.
|
||||
|
||||
Talking to Tirso in Albero will tell you the selected ending for the current game.
|
||||
|
||||
Ending A: Collect all thorn upgrades.
|
||||
Ending C: Collect all thorn upgrades and the Holy Wound of Abnegation."""
|
||||
|
||||
Ending C: Collect all thorn upgrades and the Holy Wound of Abnegation.
|
||||
"""
|
||||
display_name = "Ending"
|
||||
option_any_ending = 0
|
||||
option_ending_a = 1
|
||||
@@ -78,14 +94,18 @@ class Ending(Choice):
|
||||
|
||||
|
||||
class SkipLongQuests(Toggle):
|
||||
"""Ensures that the rewards for long quests will be filler items.
|
||||
Affected locations: \"Albero: Donate 50000 Tears\", \"Ossuary: 11th reward\", \"AtTotS: Miriam's gift\",
|
||||
\"TSC: Jocinero's final reward\""""
|
||||
"""
|
||||
Ensures that the rewards for long quests will be filler items.
|
||||
|
||||
Affected locations: "Albero: Donate 50000 Tears", "Ossuary: 11th reward", "AtTotS: Miriam's gift", "TSC: Jocinero's final reward"
|
||||
"""
|
||||
display_name = "Skip Long Quests"
|
||||
|
||||
|
||||
class ThornShuffle(Choice):
|
||||
"""Shuffles the Thorn given by Deogracias and all Thorn upgrades into the item pool."""
|
||||
"""
|
||||
Shuffles the Thorn given by Deogracias and all Thorn upgrades into the item pool.
|
||||
"""
|
||||
display_name = "Shuffle Thorn"
|
||||
option_anywhere = 0
|
||||
option_local_only = 1
|
||||
@@ -94,50 +114,68 @@ class ThornShuffle(Choice):
|
||||
|
||||
|
||||
class DashShuffle(Toggle):
|
||||
"""Turns the ability to dash into an item that must be found in the multiworld."""
|
||||
"""
|
||||
Turns the ability to dash into an item that must be found in the multiworld.
|
||||
"""
|
||||
display_name = "Shuffle Dash"
|
||||
|
||||
|
||||
class WallClimbShuffle(Toggle):
|
||||
"""Turns the ability to climb walls with your sword into an item that must be found in the multiworld."""
|
||||
"""
|
||||
Turns the ability to climb walls with your sword into an item that must be found in the multiworld.
|
||||
"""
|
||||
display_name = "Shuffle Wall Climb"
|
||||
|
||||
|
||||
class ReliquaryShuffle(DefaultOnToggle):
|
||||
"""Adds the True Torment exclusive Reliquary rosary beads into the item pool."""
|
||||
"""
|
||||
Adds the True Torment exclusive Reliquary rosary beads into the item pool.
|
||||
"""
|
||||
display_name = "Shuffle Penitence Rewards"
|
||||
|
||||
|
||||
class CustomItem1(Toggle):
|
||||
"""Adds the custom relic Boots of Pleading into the item pool, which grants the ability to fall onto spikes
|
||||
and survive.
|
||||
Must have the \"Blasphemous-Boots-of-Pleading\" mod installed to connect to a multiworld."""
|
||||
"""
|
||||
Adds the custom relic Boots of Pleading into the item pool, which grants the ability to fall onto spikes and survive.
|
||||
|
||||
Must have the "Boots of Pleading" mod installed to connect to a multiworld.
|
||||
"""
|
||||
display_name = "Boots of Pleading"
|
||||
|
||||
|
||||
class CustomItem2(Toggle):
|
||||
"""Adds the custom relic Purified Hand of the Nun into the item pool, which grants the ability to jump
|
||||
a second time in mid-air.
|
||||
Must have the \"Blasphemous-Double-Jump\" mod installed to connect to a multiworld."""
|
||||
"""
|
||||
Adds the custom relic Purified Hand of the Nun into the item pool, which grants the ability to jump a second time in mid-air.
|
||||
|
||||
Must have the "Double Jump" mod installed to connect to a multiworld.
|
||||
"""
|
||||
display_name = "Purified Hand of the Nun"
|
||||
|
||||
|
||||
class StartWheel(Toggle):
|
||||
"""Changes the beginning gift to The Young Mason's Wheel."""
|
||||
"""
|
||||
Changes the beginning gift to The Young Mason's Wheel.
|
||||
"""
|
||||
display_name = "Start with Wheel"
|
||||
|
||||
|
||||
class SkillRando(Toggle):
|
||||
"""Randomizes the abilities from the skill tree into the item pool."""
|
||||
"""
|
||||
Randomizes the abilities from the skill tree into the item pool.
|
||||
"""
|
||||
display_name = "Skill Randomizer"
|
||||
|
||||
|
||||
class EnemyRando(Choice):
|
||||
"""Randomizes the enemies that appear in each room.
|
||||
Shuffled: Enemies will be shuffled amongst each other, but can only appear as many times as they do in
|
||||
a standard game.
|
||||
"""
|
||||
Randomizes the enemies that appear in each room.
|
||||
|
||||
Shuffled: Enemies will be shuffled amongst each other, but can only appear as many times as they do in a standard game.
|
||||
|
||||
Randomized: Every enemy is completely random, and can appear any number of times.
|
||||
Some enemies will never be randomized."""
|
||||
|
||||
Some enemies will never be randomized.
|
||||
"""
|
||||
display_name = "Enemy Randomizer"
|
||||
option_disabled = 0
|
||||
option_shuffled = 1
|
||||
@@ -146,43 +184,75 @@ class EnemyRando(Choice):
|
||||
|
||||
|
||||
class EnemyGroups(DefaultOnToggle):
|
||||
"""Randomized enemies will chosen from sets of specific groups.
|
||||
"""
|
||||
Randomized enemies will be chosen from sets of specific groups.
|
||||
|
||||
(Weak, normal, large, flying)
|
||||
Has no effect if Enemy Randomizer is disabled."""
|
||||
|
||||
Has no effect if Enemy Randomizer is disabled.
|
||||
"""
|
||||
display_name = "Enemy Groups"
|
||||
|
||||
|
||||
class EnemyScaling(DefaultOnToggle):
|
||||
"""Randomized enemies will have their stats increased or decreased depending on the area they appear in.
|
||||
Has no effect if Enemy Randomizer is disabled."""
|
||||
"""
|
||||
Randomized enemies will have their stats increased or decreased depending on the area they appear in.
|
||||
|
||||
Has no effect if Enemy Randomizer is disabled.
|
||||
"""
|
||||
display_name = "Enemy Scaling"
|
||||
|
||||
|
||||
class BlasphemousDeathLink(DeathLink):
|
||||
"""When you die, everyone dies. The reverse is also true.
|
||||
Note that Guilt Fragments will not appear when killed by Death Link."""
|
||||
"""
|
||||
When you die, everyone dies. The reverse is also true.
|
||||
|
||||
Note that Guilt Fragments will not appear when killed by Death Link.
|
||||
"""
|
||||
|
||||
|
||||
blasphemous_options = {
|
||||
"prie_dieu_warp": PrieDieuWarp,
|
||||
"skip_cutscenes": SkipCutscenes,
|
||||
"corpse_hints": CorpseHints,
|
||||
"difficulty": Difficulty,
|
||||
"penitence": Penitence,
|
||||
"starting_location": StartingLocation,
|
||||
"ending": Ending,
|
||||
"skip_long_quests": SkipLongQuests,
|
||||
"thorn_shuffle" : ThornShuffle,
|
||||
"dash_shuffle": DashShuffle,
|
||||
"wall_climb_shuffle": WallClimbShuffle,
|
||||
"reliquary_shuffle": ReliquaryShuffle,
|
||||
"boots_of_pleading": CustomItem1,
|
||||
"purified_hand": CustomItem2,
|
||||
"start_wheel": StartWheel,
|
||||
"skill_randomizer": SkillRando,
|
||||
"enemy_randomizer": EnemyRando,
|
||||
"enemy_groups": EnemyGroups,
|
||||
"enemy_scaling": EnemyScaling,
|
||||
"death_link": BlasphemousDeathLink,
|
||||
"start_inventory": StartInventoryPool
|
||||
}
|
||||
@dataclass
|
||||
class BlasphemousOptions(PerGameCommonOptions):
|
||||
prie_dieu_warp: PrieDieuWarp
|
||||
skip_cutscenes: SkipCutscenes
|
||||
corpse_hints: CorpseHints
|
||||
difficulty: Difficulty
|
||||
penitence: Penitence
|
||||
starting_location: StartingLocation
|
||||
ending: Ending
|
||||
skip_long_quests: SkipLongQuests
|
||||
thorn_shuffle: ThornShuffle
|
||||
dash_shuffle: DashShuffle
|
||||
wall_climb_shuffle: WallClimbShuffle
|
||||
reliquary_shuffle: ReliquaryShuffle
|
||||
boots_of_pleading: CustomItem1
|
||||
purified_hand: CustomItem2
|
||||
start_wheel: StartWheel
|
||||
skill_randomizer: SkillRando
|
||||
enemy_randomizer: EnemyRando
|
||||
enemy_groups: EnemyGroups
|
||||
enemy_scaling: EnemyScaling
|
||||
death_link: BlasphemousDeathLink
|
||||
|
||||
|
||||
blas_option_groups = [
|
||||
OptionGroup("Quality of Life", [
|
||||
PrieDieuWarp,
|
||||
SkipCutscenes,
|
||||
CorpseHints,
|
||||
SkipLongQuests,
|
||||
StartWheel
|
||||
]),
|
||||
OptionGroup("Moveset", [
|
||||
DashShuffle,
|
||||
WallClimbShuffle,
|
||||
SkillRando,
|
||||
CustomItem1,
|
||||
CustomItem2
|
||||
]),
|
||||
OptionGroup("Enemy Randomizer", [
|
||||
EnemyRando,
|
||||
EnemyGroups,
|
||||
EnemyScaling
|
||||
])
|
||||
]
|
||||
|
||||
582
worlds/blasphemous/Preprocessor.py
Normal file
582
worlds/blasphemous/Preprocessor.py
Normal file
@@ -0,0 +1,582 @@
|
||||
# Preprocessor to convert Blasphemous Randomizer logic into a StringWorldDefinition for use with APHKLogicExtractor
|
||||
# https://github.com/BrandenEK/Blasphemous.Randomizer
|
||||
# https://github.com/ArchipelagoMW-HollowKnight/APHKLogicExtractor
|
||||
|
||||
|
||||
import json, requests, argparse
|
||||
from typing import List, Dict, Any
|
||||
|
||||
|
||||
def load_resource_local(file: str) -> List[Dict[str, Any]]:
|
||||
print(f"Reading from {file}")
|
||||
loaded = []
|
||||
with open(file, encoding="utf-8") as f:
|
||||
loaded = read_json(f.readlines())
|
||||
f.close()
|
||||
|
||||
return loaded
|
||||
|
||||
|
||||
def load_resource_from_web(url: str) -> List[Dict[str, Any]]:
|
||||
req = requests.get(url, timeout=1)
|
||||
print(f"Reading from {url}")
|
||||
req.encoding = "utf-8"
|
||||
lines: List[str] = []
|
||||
for line in req.text.splitlines():
|
||||
while "\t" in line:
|
||||
line = line[1::]
|
||||
if line != "":
|
||||
lines.append(line)
|
||||
return read_json(lines)
|
||||
|
||||
|
||||
def read_json(lines: List[str]) -> List[Dict[str, Any]]:
|
||||
loaded = []
|
||||
creating_object: bool = False
|
||||
obj: str = ""
|
||||
for line in lines:
|
||||
stripped = line.strip()
|
||||
if "{" in stripped:
|
||||
creating_object = True
|
||||
obj += stripped
|
||||
continue
|
||||
elif "}," in stripped or "}" in stripped and "]" in lines[lines.index(line)+1]:
|
||||
creating_object = False
|
||||
obj += "}"
|
||||
#print(f"obj = {obj}")
|
||||
loaded.append(json.loads(obj))
|
||||
obj = ""
|
||||
continue
|
||||
|
||||
if not creating_object:
|
||||
continue
|
||||
else:
|
||||
try:
|
||||
if "}," in lines[lines.index(line)+1] and stripped[-1] == ",":
|
||||
obj += stripped[:-1]
|
||||
else:
|
||||
obj += stripped
|
||||
except IndexError:
|
||||
obj += stripped
|
||||
|
||||
return loaded
|
||||
|
||||
|
||||
def get_room_from_door(door: str) -> str:
|
||||
return door[:door.find("[")]
|
||||
|
||||
|
||||
def preprocess_logic(is_door: bool, id: str, logic: str) -> str:
|
||||
if id in logic and not is_door:
|
||||
index: int = logic.find(id)
|
||||
logic = logic[:index] + logic[index+len(id)+4:]
|
||||
|
||||
while ">=" in logic:
|
||||
index: int = logic.find(">=")
|
||||
logic = logic[:index-1] + logic[index+3:]
|
||||
|
||||
while ">" in logic:
|
||||
index: int = logic.find(">")
|
||||
count = int(logic[index+2])
|
||||
count += 1
|
||||
logic = logic[:index-1] + str(count) + logic[index+3:]
|
||||
|
||||
while "<=" in logic:
|
||||
index: int = logic.find("<=")
|
||||
logic = logic[:index-1] + logic[index+3:]
|
||||
|
||||
while "<" in logic:
|
||||
index: int = logic.find("<")
|
||||
count = int(logic[index+2])
|
||||
count += 1
|
||||
logic = logic[:index-1] + str(count) + logic[index+3:]
|
||||
|
||||
#print(logic)
|
||||
return logic
|
||||
|
||||
|
||||
def build_logic_conditions(logic: str) -> List[List[str]]:
|
||||
all_conditions: List[List[str]] = []
|
||||
|
||||
parts = logic.split()
|
||||
sub_part: str = ""
|
||||
current_index: int = 0
|
||||
parens: int = -1
|
||||
current_condition: List[str] = []
|
||||
parens_conditions: List[List[List[str]]] = []
|
||||
|
||||
for index, part in enumerate(parts):
|
||||
#print(current_index, index, parens, part)
|
||||
|
||||
# skip parts that have already been handled
|
||||
if index < current_index:
|
||||
continue
|
||||
|
||||
# break loop if reached final part
|
||||
try:
|
||||
parts[index+1]
|
||||
except IndexError:
|
||||
#print("INDEXERROR", part)
|
||||
if parens < 0:
|
||||
current_condition.append(part)
|
||||
if len(parens_conditions) > 0:
|
||||
for i in parens_conditions:
|
||||
for j in i:
|
||||
all_conditions.append(j + current_condition)
|
||||
else:
|
||||
all_conditions.append(current_condition)
|
||||
break
|
||||
|
||||
#print(current_condition, parens, sub_part)
|
||||
|
||||
# prepare for subcondition
|
||||
if "(" in part:
|
||||
# keep track of nested parentheses
|
||||
if parens == -1:
|
||||
parens = 0
|
||||
for char in part:
|
||||
if char == "(":
|
||||
parens += 1
|
||||
|
||||
# add to sub part
|
||||
if sub_part == "":
|
||||
sub_part = part
|
||||
else:
|
||||
sub_part += f" {part}"
|
||||
#if not ")" in part:
|
||||
continue
|
||||
|
||||
# end of subcondition
|
||||
if ")" in part:
|
||||
# read every character in case of multiple closing parentheses
|
||||
for char in part:
|
||||
if char == ")":
|
||||
parens -= 1
|
||||
|
||||
sub_part += f" {part}"
|
||||
|
||||
# if reached end of parentheses, handle subcondition
|
||||
if parens == 0:
|
||||
#print(current_condition, sub_part)
|
||||
parens = -1
|
||||
|
||||
try:
|
||||
parts[index+1]
|
||||
except IndexError:
|
||||
#print("END OF LOGIC")
|
||||
if len(parens_conditions) > 0:
|
||||
parens_conditions.append(build_logic_subconditions(current_condition, sub_part))
|
||||
#print("PARENS:", parens_conditions)
|
||||
|
||||
temp_conditions: List[List[str]] = []
|
||||
|
||||
for i in parens_conditions[0]:
|
||||
for j in parens_conditions[1]:
|
||||
temp_conditions.append(i + j)
|
||||
|
||||
parens_conditions.pop(0)
|
||||
parens_conditions.pop(0)
|
||||
|
||||
while len(parens_conditions) > 0:
|
||||
temp_conditions2 = temp_conditions
|
||||
temp_conditions = []
|
||||
for k in temp_conditions2:
|
||||
for l in parens_conditions[0]:
|
||||
temp_conditions.append(k + l)
|
||||
|
||||
parens_conditions.pop(0)
|
||||
|
||||
#print("TEMP:", remove_duplicates(temp_conditions))
|
||||
all_conditions += temp_conditions
|
||||
else:
|
||||
all_conditions += build_logic_subconditions(current_condition, sub_part)
|
||||
else:
|
||||
#print("NEXT PARTS:", parts[index+1], parts[index+2])
|
||||
if parts[index+1] == "&&":
|
||||
parens_conditions.append(build_logic_subconditions(current_condition, sub_part))
|
||||
#print("PARENS:", parens_conditions)
|
||||
else:
|
||||
if len(parens_conditions) > 0:
|
||||
parens_conditions.append(build_logic_subconditions(current_condition, sub_part))
|
||||
#print("PARENS:", parens_conditions)
|
||||
|
||||
temp_conditions: List[List[str]] = []
|
||||
|
||||
for i in parens_conditions[0]:
|
||||
for j in parens_conditions[1]:
|
||||
temp_conditions.append(i + j)
|
||||
|
||||
parens_conditions.pop(0)
|
||||
parens_conditions.pop(0)
|
||||
|
||||
while len(parens_conditions) > 0:
|
||||
temp_conditions2 = temp_conditions
|
||||
temp_conditions = []
|
||||
for k in temp_conditions2:
|
||||
for l in parens_conditions[0]:
|
||||
temp_conditions.append(k + l)
|
||||
|
||||
parens_conditions.pop(0)
|
||||
|
||||
#print("TEMP:", remove_duplicates(temp_conditions))
|
||||
all_conditions += temp_conditions
|
||||
else:
|
||||
all_conditions += build_logic_subconditions(current_condition, sub_part)
|
||||
|
||||
current_index = index+2
|
||||
|
||||
current_condition = []
|
||||
sub_part = ""
|
||||
|
||||
continue
|
||||
|
||||
# collect all parts until reaching end of parentheses
|
||||
if parens > 0:
|
||||
sub_part += f" {part}"
|
||||
continue
|
||||
|
||||
current_condition.append(part)
|
||||
|
||||
# continue with current condition
|
||||
if parts[index+1] == "&&":
|
||||
current_index = index+2
|
||||
continue
|
||||
|
||||
# add condition to list and start new one
|
||||
elif parts[index+1] == "||":
|
||||
if len(parens_conditions) > 0:
|
||||
for i in parens_conditions:
|
||||
for j in i:
|
||||
all_conditions.append(j + current_condition)
|
||||
parens_conditions = []
|
||||
else:
|
||||
all_conditions.append(current_condition)
|
||||
current_condition = []
|
||||
current_index = index+2
|
||||
continue
|
||||
|
||||
return remove_duplicates(all_conditions)
|
||||
|
||||
|
||||
def build_logic_subconditions(current_condition: List[str], subcondition: str) -> List[List[str]]:
|
||||
#print("STARTED SUBCONDITION", current_condition, subcondition)
|
||||
subconditions = build_logic_conditions(subcondition[1:-1])
|
||||
final_conditions = []
|
||||
|
||||
for condition in subconditions:
|
||||
final_condition = current_condition + condition
|
||||
final_conditions.append(final_condition)
|
||||
|
||||
#print("ENDED SUBCONDITION")
|
||||
#print(final_conditions)
|
||||
return final_conditions
|
||||
|
||||
|
||||
def remove_duplicates(conditions: List[List[str]]) -> List[List[str]]:
|
||||
final_conditions: List[List[str]] = []
|
||||
for condition in conditions:
|
||||
final_conditions.append(list(dict.fromkeys(condition)))
|
||||
|
||||
return final_conditions
|
||||
|
||||
|
||||
def handle_door_visibility(door: Dict[str, Any]) -> Dict[str, Any]:
|
||||
if door.get("visibilityFlags") == None:
|
||||
return door
|
||||
else:
|
||||
flags: List[str] = str(door.get("visibilityFlags")).split(", ")
|
||||
#print(flags)
|
||||
temp_flags: List[str] = []
|
||||
this_door: bool = False
|
||||
#required_doors: str = ""
|
||||
|
||||
if "ThisDoor" in flags:
|
||||
this_door = True
|
||||
|
||||
#if "requiredDoors" in flags:
|
||||
# required_doors: str = " || ".join(door.get("requiredDoors"))
|
||||
|
||||
if "DoubleJump" in flags:
|
||||
temp_flags.append("DoubleJump")
|
||||
|
||||
if "NormalLogic" in flags:
|
||||
temp_flags.append("NormalLogic")
|
||||
|
||||
if "NormalLogicAndDoubleJump" in flags:
|
||||
temp_flags.append("NormalLogicAndDoubleJump")
|
||||
|
||||
if "HardLogic" in flags:
|
||||
temp_flags.append("HardLogic")
|
||||
|
||||
if "HardLogicAndDoubleJump" in flags:
|
||||
temp_flags.append("HardLogicAndDoubleJump")
|
||||
|
||||
if "EnemySkips" in flags:
|
||||
temp_flags.append("EnemySkips")
|
||||
|
||||
if "EnemySkipsAndDoubleJump" in flags:
|
||||
temp_flags.append("EnemySkipsAndDoubleJump")
|
||||
|
||||
# remove duplicates
|
||||
temp_flags = list(dict.fromkeys(temp_flags))
|
||||
|
||||
original_logic: str = door.get("logic")
|
||||
temp_logic: str = ""
|
||||
|
||||
if this_door:
|
||||
temp_logic = door.get("id")
|
||||
|
||||
if temp_flags != []:
|
||||
if temp_logic != "":
|
||||
temp_logic += " || "
|
||||
temp_logic += ' && '.join(temp_flags)
|
||||
|
||||
if temp_logic != "" and original_logic != None:
|
||||
if len(original_logic.split()) == 1:
|
||||
if len(temp_logic.split()) == 1:
|
||||
door["logic"] = f"{temp_logic} && {original_logic}"
|
||||
else:
|
||||
door["logic"] = f"({temp_logic}) && {original_logic}"
|
||||
else:
|
||||
if len(temp_logic.split()) == 1:
|
||||
door["logic"] = f"{temp_logic} && ({original_logic})"
|
||||
else:
|
||||
door["logic"] = f"({temp_logic}) && ({original_logic})"
|
||||
elif temp_logic != "" and original_logic == None:
|
||||
door["logic"] = temp_logic
|
||||
|
||||
return door
|
||||
|
||||
|
||||
def get_state_provider_for_condition(condition: List[str]) -> str:
|
||||
for item in condition:
|
||||
if (item[0] == "D" and item[3] == "Z" and item[6] == "S")\
|
||||
or (item[0] == "D" and item[3] == "B" and item[4] == "Z" and item[7] == "S"):
|
||||
return item
|
||||
return None
|
||||
|
||||
|
||||
def parse_args() -> argparse.Namespace:
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument('-l', '--local', action="store_true", help="Use local files in the same directory instead of reading resource files from the BrandenEK/Blasphemous-Randomizer repository.")
|
||||
args = parser.parse_args()
|
||||
return args
|
||||
|
||||
|
||||
def main(args: argparse.Namespace):
|
||||
doors = []
|
||||
locations = []
|
||||
|
||||
if (args.local):
|
||||
doors = load_resource_local("doors.json")
|
||||
locations = load_resource_local("locations_items.json")
|
||||
|
||||
else:
|
||||
doors = load_resource_from_web("https://raw.githubusercontent.com/BrandenEK/Blasphemous-Randomizer/main/resources/data/Randomizer/doors.json")
|
||||
locations = load_resource_from_web("https://raw.githubusercontent.com/BrandenEK/Blasphemous-Randomizer/main/resources/data/Randomizer/locations_items.json")
|
||||
|
||||
original_connections: Dict[str, str] = {}
|
||||
rooms: Dict[str, List[str]] = {}
|
||||
output: Dict[str, Any] = {}
|
||||
logic_objects: List[Dict[str, Any]] = []
|
||||
|
||||
for door in doors:
|
||||
if door.get("originalDoor") != None:
|
||||
if not door.get("id") in original_connections:
|
||||
original_connections[door.get("id")] = door.get("originalDoor")
|
||||
original_connections[door.get("originalDoor")] = door.get("id")
|
||||
|
||||
room: str = get_room_from_door(door.get("originalDoor"))
|
||||
if not room in rooms.keys():
|
||||
rooms[room] = [door.get("id")]
|
||||
else:
|
||||
rooms[room].append(door.get("id"))
|
||||
|
||||
def flip_doors_in_condition(condition: List[str]) -> List[str]:
|
||||
new_condition = []
|
||||
for item in condition:
|
||||
if item in original_connections:
|
||||
new_condition.append(original_connections[item])
|
||||
else:
|
||||
new_condition.append(item)
|
||||
|
||||
return new_condition
|
||||
|
||||
for room in rooms.keys():
|
||||
obj = {
|
||||
"Name": room,
|
||||
"Logic": [],
|
||||
"Handling": "Default"
|
||||
}
|
||||
|
||||
for door in rooms[room]:
|
||||
logic = {
|
||||
"StateProvider": door,
|
||||
"Conditions": [],
|
||||
"StateModifiers": []
|
||||
}
|
||||
obj["Logic"].append(logic)
|
||||
|
||||
logic_objects.append(obj)
|
||||
|
||||
for door in doors:
|
||||
if door.get("direction") == 5:
|
||||
continue
|
||||
|
||||
handling: str = "Transition"
|
||||
if "Cell" in door.get("id"):
|
||||
handling = "Default"
|
||||
obj = {
|
||||
"Name": door.get("id"),
|
||||
"Logic": [],
|
||||
"Handling": handling
|
||||
}
|
||||
|
||||
visibility_flags: List[str] = []
|
||||
if door.get("visibilityFlags") != None:
|
||||
visibility_flags = str(door.get("visibilityFlags")).split(", ")
|
||||
if "1" in visibility_flags:
|
||||
visibility_flags.remove("1")
|
||||
visibility_flags.append("ThisDoor")
|
||||
|
||||
required_doors: List[str] = []
|
||||
if door.get("requiredDoors"):
|
||||
required_doors = door.get("requiredDoors")
|
||||
|
||||
if len(visibility_flags) > 0:
|
||||
for flag in visibility_flags:
|
||||
if flag == "RequiredDoors":
|
||||
continue
|
||||
|
||||
if flag == "ThisDoor":
|
||||
flag = original_connections[door.get("id")]
|
||||
|
||||
if door.get("logic") != None:
|
||||
logic: str = door.get("logic")
|
||||
logic = f"{flag} && ({logic})"
|
||||
logic = preprocess_logic(True, door.get("id"), logic)
|
||||
conditions = build_logic_conditions(logic)
|
||||
for condition in conditions:
|
||||
condition = flip_doors_in_condition(condition)
|
||||
state_provider: str = get_room_from_door(door.get("id"))
|
||||
|
||||
if get_state_provider_for_condition(condition) != None:
|
||||
state_provider = get_state_provider_for_condition(condition)
|
||||
condition.remove(state_provider)
|
||||
|
||||
logic = {
|
||||
"StateProvider": state_provider,
|
||||
"Conditions": condition,
|
||||
"StateModifiers": []
|
||||
}
|
||||
obj["Logic"].append(logic)
|
||||
else:
|
||||
logic = {
|
||||
"StateProvider": get_room_from_door(door.get("id")),
|
||||
"Conditions": [flag],
|
||||
"StateModifiers": []
|
||||
}
|
||||
obj["Logic"].append(logic)
|
||||
|
||||
if "RequiredDoors" in visibility_flags:
|
||||
for d in required_doors:
|
||||
flipped = original_connections[d]
|
||||
if door.get("logic") != None:
|
||||
logic: str = preprocess_logic(True, door.get("id"), door.get("logic"))
|
||||
conditions = build_logic_conditions(logic)
|
||||
for condition in conditions:
|
||||
condition = flip_doors_in_condition(condition)
|
||||
state_provider: str = flipped
|
||||
|
||||
if flipped in condition:
|
||||
condition.remove(flipped)
|
||||
|
||||
logic = {
|
||||
"StateProvider": state_provider,
|
||||
"Conditions": condition,
|
||||
"StateModifiers": []
|
||||
}
|
||||
obj["Logic"].append(logic)
|
||||
else:
|
||||
logic = {
|
||||
"StateProvider": flipped,
|
||||
"Conditions": [],
|
||||
"StateModifiers": []
|
||||
}
|
||||
obj["Logic"].append(logic)
|
||||
|
||||
else:
|
||||
if door.get("logic") != None:
|
||||
logic: str = preprocess_logic(True, door.get("id"), door.get("logic"))
|
||||
conditions = build_logic_conditions(logic)
|
||||
for condition in conditions:
|
||||
condition = flip_doors_in_condition(condition)
|
||||
stateProvider: str = get_room_from_door(door.get("id"))
|
||||
|
||||
if get_state_provider_for_condition(condition) != None:
|
||||
stateProvider = get_state_provider_for_condition(condition)
|
||||
condition.remove(stateProvider)
|
||||
|
||||
logic = {
|
||||
"StateProvider": stateProvider,
|
||||
"Conditions": condition,
|
||||
"StateModifiers": []
|
||||
}
|
||||
obj["Logic"].append(logic)
|
||||
else:
|
||||
logic = {
|
||||
"StateProvider": get_room_from_door(door.get("id")),
|
||||
"Conditions": [],
|
||||
"StateModifiers": []
|
||||
}
|
||||
obj["Logic"].append(logic)
|
||||
|
||||
logic_objects.append(obj)
|
||||
|
||||
for location in locations:
|
||||
obj = {
|
||||
"Name": location.get("id"),
|
||||
"Logic": [],
|
||||
"Handling": "Location"
|
||||
}
|
||||
|
||||
if location.get("logic") != None:
|
||||
for condition in build_logic_conditions(preprocess_logic(False, location.get("id"), location.get("logic"))):
|
||||
condition = flip_doors_in_condition(condition)
|
||||
stateProvider: str = location.get("room")
|
||||
|
||||
if get_state_provider_for_condition(condition) != None:
|
||||
stateProvider = get_state_provider_for_condition(condition)
|
||||
condition.remove(stateProvider)
|
||||
|
||||
if stateProvider == "Initial":
|
||||
stateProvider = None
|
||||
|
||||
logic = {
|
||||
"StateProvider": stateProvider,
|
||||
"Conditions": condition,
|
||||
"StateModifiers": []
|
||||
}
|
||||
obj["Logic"].append(logic)
|
||||
else:
|
||||
stateProvider: str = location.get("room")
|
||||
if stateProvider == "Initial":
|
||||
stateProvider = None
|
||||
logic = {
|
||||
"StateProvider": stateProvider,
|
||||
"Conditions": [],
|
||||
"StateModifiers": []
|
||||
}
|
||||
obj["Logic"].append(logic)
|
||||
|
||||
logic_objects.append(obj)
|
||||
|
||||
output["LogicObjects"] = logic_objects
|
||||
|
||||
with open("StringWorldDefinition.json", "w") as file:
|
||||
print("Writing to StringWorldDefinition.json")
|
||||
file.write(json.dumps(output, indent=4))
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main(parse_args())
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@@ -8,12 +8,12 @@ unrandomized_dict: Dict[str, str] = {
|
||||
}
|
||||
|
||||
|
||||
junk_locations: Set[str] = [
|
||||
junk_locations: Set[str] = {
|
||||
"Albero: Donate 50000 Tears",
|
||||
"Ossuary: 11th reward",
|
||||
"AtTotS: Miriam's gift",
|
||||
"TSC: Jocinero's final reward"
|
||||
]
|
||||
}
|
||||
|
||||
|
||||
thorn_set: Set[str] = {
|
||||
@@ -44,4 +44,4 @@ skill_dict: Dict[str, str] = {
|
||||
"Skill 5, Tier 1": "Lunge Skill",
|
||||
"Skill 5, Tier 2": "Lunge Skill",
|
||||
"Skill 5, Tier 3": "Lunge Skill",
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,15 +1,15 @@
|
||||
from typing import Dict, List, Set, Any
|
||||
from collections import Counter
|
||||
from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification
|
||||
from BaseClasses import Region, Location, Item, Tutorial, ItemClassification
|
||||
from Options import OptionError
|
||||
from worlds.AutoWorld import World, WebWorld
|
||||
from .Items import base_id, item_table, group_table, tears_set, reliquary_set, event_table
|
||||
from .Locations import location_table
|
||||
from .Rooms import room_table, door_table
|
||||
from .Rules import rules
|
||||
from worlds.generic.Rules import set_rule, add_rule
|
||||
from .Options import blasphemous_options
|
||||
from .Items import base_id, item_table, group_table, tears_list, reliquary_set
|
||||
from .Locations import location_names
|
||||
from .Rules import BlasRules
|
||||
from worlds.generic.Rules import set_rule
|
||||
from .Options import BlasphemousOptions, blas_option_groups
|
||||
from .Vanilla import unrandomized_dict, junk_locations, thorn_set, skill_dict
|
||||
|
||||
from .region_data import regions, locations
|
||||
|
||||
class BlasphemousWeb(WebWorld):
|
||||
theme = "stone"
|
||||
@@ -21,39 +21,33 @@ class BlasphemousWeb(WebWorld):
|
||||
"setup/en",
|
||||
["TRPG"]
|
||||
)]
|
||||
option_groups = blas_option_groups
|
||||
|
||||
|
||||
class BlasphemousWorld(World):
|
||||
"""
|
||||
Blasphemous is a challenging Metroidvania set in the cursed land of Cvstodia. Play as the Penitent One, trapped
|
||||
in an endless cycle of death and rebirth, and free the world from it's terrible fate in your quest to break
|
||||
in an endless cycle of death and rebirth, and free the world from its terrible fate in your quest to break
|
||||
your eternal damnation!
|
||||
"""
|
||||
|
||||
game: str = "Blasphemous"
|
||||
game = "Blasphemous"
|
||||
web = BlasphemousWeb()
|
||||
|
||||
item_name_to_id = {item["name"]: (base_id + index) for index, item in enumerate(item_table)}
|
||||
location_name_to_id = {loc["name"]: (base_id + index) for index, loc in enumerate(location_table)}
|
||||
location_name_to_game_id = {loc["name"]: loc["game_id"] for loc in location_table}
|
||||
location_name_to_id = {loc: (base_id + index) for index, loc in enumerate(location_names.values())}
|
||||
|
||||
item_name_groups = group_table
|
||||
option_definitions = blasphemous_options
|
||||
options_dataclass = BlasphemousOptions
|
||||
options: BlasphemousOptions
|
||||
|
||||
required_client_version = (0, 4, 2)
|
||||
required_client_version = (0, 4, 7)
|
||||
|
||||
|
||||
def __init__(self, multiworld, player):
|
||||
super(BlasphemousWorld, self).__init__(multiworld, player)
|
||||
self.start_room: str = "D17Z01S01"
|
||||
self.door_connections: Dict[str, str] = {}
|
||||
|
||||
|
||||
def set_rules(self):
|
||||
rules(self)
|
||||
for door in door_table:
|
||||
add_rule(self.multiworld.get_location(door["Id"], self.player),
|
||||
lambda state: state.can_reach(self.get_connected_door(door["Id"])), self.player)
|
||||
self.disabled_locations: List[str] = []
|
||||
|
||||
|
||||
def create_item(self, name: str) -> "BlasphemousItem":
|
||||
@@ -68,64 +62,56 @@ class BlasphemousWorld(World):
|
||||
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
return self.multiworld.random.choice(tears_set)
|
||||
return self.random.choice(tears_list)
|
||||
|
||||
|
||||
def generate_early(self):
|
||||
world = self.multiworld
|
||||
player = self.player
|
||||
if not self.options.starting_location.randomized:
|
||||
if self.options.starting_location == "mourning_havoc" and self.options.difficulty < 2:
|
||||
raise OptionError(f"[Blasphemous - '{self.player_name}'] "
|
||||
f"{self.options.starting_location} cannot be chosen if Difficulty is lower than Hard.")
|
||||
|
||||
if not world.starting_location[player].randomized:
|
||||
if world.starting_location[player].value == 6 and world.difficulty[player].value < 2:
|
||||
raise Exception(f"[Blasphemous - '{world.get_player_name(player)}'] {world.starting_location[player]}"
|
||||
" cannot be chosen if Difficulty is lower than Hard.")
|
||||
|
||||
if (world.starting_location[player].value == 0 or world.starting_location[player].value == 6) \
|
||||
and world.dash_shuffle[player]:
|
||||
raise Exception(f"[Blasphemous - '{world.get_player_name(player)}'] {world.starting_location[player]}"
|
||||
" cannot be chosen if Shuffle Dash is enabled.")
|
||||
if (self.options.starting_location == "brotherhood" or self.options.starting_location == "mourning_havoc") \
|
||||
and self.options.dash_shuffle:
|
||||
raise OptionError(f"[Blasphemous - '{self.player_name}'] "
|
||||
f"{self.options.starting_location} cannot be chosen if Shuffle Dash is enabled.")
|
||||
|
||||
if world.starting_location[player].value == 3 and world.wall_climb_shuffle[player]:
|
||||
raise Exception(f"[Blasphemous - '{world.get_player_name(player)}'] {world.starting_location[player]}"
|
||||
" cannot be chosen if Shuffle Wall Climb is enabled.")
|
||||
if self.options.starting_location == "grievance" and self.options.wall_climb_shuffle:
|
||||
raise OptionError(f"[Blasphemous - '{self.player_name}'] "
|
||||
f"{self.options.starting_location} cannot be chosen if Shuffle Wall Climb is enabled.")
|
||||
else:
|
||||
locations: List[int] = [ 0, 1, 2, 3, 4, 5, 6 ]
|
||||
invalid: bool = False
|
||||
|
||||
if world.difficulty[player].value < 2:
|
||||
if self.options.difficulty < 2:
|
||||
locations.remove(6)
|
||||
|
||||
if world.dash_shuffle[player]:
|
||||
if self.options.dash_shuffle:
|
||||
locations.remove(0)
|
||||
if 6 in locations:
|
||||
locations.remove(6)
|
||||
|
||||
if world.wall_climb_shuffle[player]:
|
||||
if self.options.wall_climb_shuffle:
|
||||
locations.remove(3)
|
||||
|
||||
if world.starting_location[player].value == 6 and world.difficulty[player].value < 2:
|
||||
invalid = True
|
||||
|
||||
if (world.starting_location[player].value == 0 or world.starting_location[player].value == 6) \
|
||||
and world.dash_shuffle[player]:
|
||||
invalid = True
|
||||
|
||||
if world.starting_location[player].value == 3 and world.wall_climb_shuffle[player]:
|
||||
invalid = True
|
||||
|
||||
if invalid:
|
||||
world.starting_location[player].value = world.random.choice(locations)
|
||||
if self.options.starting_location.value not in locations:
|
||||
self.options.starting_location.value = self.random.choice(locations)
|
||||
|
||||
|
||||
if not world.dash_shuffle[player]:
|
||||
world.push_precollected(self.create_item("Dash Ability"))
|
||||
if not self.options.dash_shuffle:
|
||||
self.multiworld.push_precollected(self.create_item("Dash Ability"))
|
||||
|
||||
if not world.wall_climb_shuffle[player]:
|
||||
world.push_precollected(self.create_item("Wall Climb Ability"))
|
||||
if not self.options.wall_climb_shuffle:
|
||||
self.multiworld.push_precollected(self.create_item("Wall Climb Ability"))
|
||||
|
||||
if world.skip_long_quests[player]:
|
||||
if not self.options.boots_of_pleading:
|
||||
self.disabled_locations.append("RE401")
|
||||
|
||||
if not self.options.purified_hand:
|
||||
self.disabled_locations.append("RE402")
|
||||
|
||||
if self.options.skip_long_quests:
|
||||
for loc in junk_locations:
|
||||
world.exclude_locations[player].value.add(loc)
|
||||
self.options.exclude_locations.value.add(loc)
|
||||
|
||||
start_rooms: Dict[int, str] = {
|
||||
0: "D17Z01S01",
|
||||
@@ -137,13 +123,10 @@ class BlasphemousWorld(World):
|
||||
6: "D20Z02S09"
|
||||
}
|
||||
|
||||
self.start_room = start_rooms[world.starting_location[player].value]
|
||||
self.start_room = start_rooms[self.options.starting_location.value]
|
||||
|
||||
|
||||
def create_items(self):
|
||||
world = self.multiworld
|
||||
player = self.player
|
||||
|
||||
removed: int = 0
|
||||
to_remove: List[str] = [
|
||||
"Tears of Atonement (250)",
|
||||
@@ -156,46 +139,46 @@ class BlasphemousWorld(World):
|
||||
skipped_items = []
|
||||
junk: int = 0
|
||||
|
||||
for item, count in world.start_inventory[player].value.items():
|
||||
for item, count in self.options.start_inventory.value.items():
|
||||
for _ in range(count):
|
||||
skipped_items.append(item)
|
||||
junk += 1
|
||||
|
||||
skipped_items.extend(unrandomized_dict.values())
|
||||
|
||||
if world.thorn_shuffle[player] == 2:
|
||||
for i in range(8):
|
||||
if self.options.thorn_shuffle == "vanilla":
|
||||
for _ in range(8):
|
||||
skipped_items.append("Thorn Upgrade")
|
||||
|
||||
if world.dash_shuffle[player]:
|
||||
if self.options.dash_shuffle:
|
||||
skipped_items.append(to_remove[removed])
|
||||
removed += 1
|
||||
elif not world.dash_shuffle[player]:
|
||||
elif not self.options.dash_shuffle:
|
||||
skipped_items.append("Dash Ability")
|
||||
|
||||
if world.wall_climb_shuffle[player]:
|
||||
if self.options.wall_climb_shuffle:
|
||||
skipped_items.append(to_remove[removed])
|
||||
removed += 1
|
||||
elif not world.wall_climb_shuffle[player]:
|
||||
elif not self.options.wall_climb_shuffle:
|
||||
skipped_items.append("Wall Climb Ability")
|
||||
|
||||
if not world.reliquary_shuffle[player]:
|
||||
if not self.options.reliquary_shuffle:
|
||||
skipped_items.extend(reliquary_set)
|
||||
elif world.reliquary_shuffle[player]:
|
||||
for i in range(3):
|
||||
elif self.options.reliquary_shuffle:
|
||||
for _ in range(3):
|
||||
skipped_items.append(to_remove[removed])
|
||||
removed += 1
|
||||
|
||||
if not world.boots_of_pleading[player]:
|
||||
if not self.options.boots_of_pleading:
|
||||
skipped_items.append("Boots of Pleading")
|
||||
|
||||
if not world.purified_hand[player]:
|
||||
if not self.options.purified_hand:
|
||||
skipped_items.append("Purified Hand of the Nun")
|
||||
|
||||
if world.start_wheel[player]:
|
||||
if self.options.start_wheel:
|
||||
skipped_items.append("The Young Mason's Wheel")
|
||||
|
||||
if not world.skill_randomizer[player]:
|
||||
if not self.options.skill_randomizer:
|
||||
skipped_items.extend(skill_dict.values())
|
||||
|
||||
counter = Counter(skipped_items)
|
||||
@@ -208,184 +191,140 @@ class BlasphemousWorld(World):
|
||||
if count <= 0:
|
||||
continue
|
||||
else:
|
||||
for i in range(count):
|
||||
for _ in range(count):
|
||||
pool.append(self.create_item(item["name"]))
|
||||
|
||||
for _ in range(junk):
|
||||
pool.append(self.create_item(self.get_filler_item_name()))
|
||||
|
||||
world.itempool += pool
|
||||
self.multiworld.itempool += pool
|
||||
|
||||
|
||||
def pre_fill(self):
|
||||
world = self.multiworld
|
||||
player = self.player
|
||||
|
||||
self.place_items_from_dict(unrandomized_dict)
|
||||
|
||||
if world.thorn_shuffle[player] == 2:
|
||||
if self.options.thorn_shuffle == "vanilla":
|
||||
self.place_items_from_set(thorn_set, "Thorn Upgrade")
|
||||
|
||||
if world.start_wheel[player]:
|
||||
world.get_location("Beginning gift", player)\
|
||||
.place_locked_item(self.create_item("The Young Mason's Wheel"))
|
||||
if self.options.start_wheel:
|
||||
self.get_location("Beginning gift").place_locked_item(self.create_item("The Young Mason's Wheel"))
|
||||
|
||||
if not world.skill_randomizer[player]:
|
||||
if not self.options.skill_randomizer:
|
||||
self.place_items_from_dict(skill_dict)
|
||||
|
||||
if world.thorn_shuffle[player] == 1:
|
||||
world.local_items[player].value.add("Thorn Upgrade")
|
||||
if self.options.thorn_shuffle == "local_only":
|
||||
self.options.local_items.value.add("Thorn Upgrade")
|
||||
|
||||
|
||||
def place_items_from_set(self, location_set: Set[str], name: str):
|
||||
for loc in location_set:
|
||||
self.multiworld.get_location(loc, self.player)\
|
||||
.place_locked_item(self.create_item(name))
|
||||
self.get_location(loc).place_locked_item(self.create_item(name))
|
||||
|
||||
|
||||
def place_items_from_dict(self, option_dict: Dict[str, str]):
|
||||
for loc, item in option_dict.items():
|
||||
self.multiworld.get_location(loc, self.player)\
|
||||
.place_locked_item(self.create_item(item))
|
||||
self.get_location(loc).place_locked_item(self.create_item(item))
|
||||
|
||||
|
||||
def create_regions(self) -> None:
|
||||
multiworld = self.multiworld
|
||||
player = self.player
|
||||
world = self.multiworld
|
||||
|
||||
created_regions: List[str] = []
|
||||
|
||||
for r in regions:
|
||||
multiworld.regions.append(Region(r["name"], player, multiworld))
|
||||
created_regions.append(r["name"])
|
||||
|
||||
self.get_region("Menu").add_exits({self.start_room: "New Game"})
|
||||
|
||||
blas_logic = BlasRules(self)
|
||||
|
||||
for r in regions:
|
||||
region = self.get_region(r["name"])
|
||||
|
||||
for e in r["exits"]:
|
||||
region.add_exits({e["target"]}, {e["target"]: blas_logic.load_rule(True, r["name"], e)})
|
||||
|
||||
for l in [l for l in r["locations"] if l not in self.disabled_locations]:
|
||||
region.add_locations({location_names[l]: self.location_name_to_id[location_names[l]]}, BlasphemousLocation)
|
||||
|
||||
for t in r["transitions"]:
|
||||
if t == r["name"]:
|
||||
continue
|
||||
|
||||
if t in created_regions:
|
||||
region.add_exits({t})
|
||||
else:
|
||||
multiworld.regions.append(Region(t, player, multiworld))
|
||||
created_regions.append(t)
|
||||
region.add_exits({t})
|
||||
|
||||
|
||||
for l in [l for l in locations if l["name"] not in self.disabled_locations]:
|
||||
location = self.get_location(location_names[l["name"]])
|
||||
set_rule(location, blas_logic.load_rule(False, l["name"], l))
|
||||
|
||||
for rname, ename in blas_logic.indirect_conditions:
|
||||
self.multiworld.register_indirect_condition(self.get_region(rname), self.get_entrance(ename))
|
||||
#from Utils import visualize_regions
|
||||
#visualize_regions(self.get_region("Menu"), "blasphemous_regions.puml")
|
||||
|
||||
menu_region = Region("Menu", player, world)
|
||||
misc_region = Region("Misc", player, world)
|
||||
world.regions += [menu_region, misc_region]
|
||||
|
||||
for room in room_table:
|
||||
region = Region(room, player, world)
|
||||
world.regions.append(region)
|
||||
|
||||
menu_region.add_exits({self.start_room: "New Game"})
|
||||
world.get_region(self.start_room, player).add_exits({"Misc": "Misc"})
|
||||
|
||||
for door in door_table:
|
||||
if door.get("OriginalDoor") is None:
|
||||
continue
|
||||
else:
|
||||
if not door["Id"] in self.door_connections.keys():
|
||||
self.door_connections[door["Id"]] = door["OriginalDoor"]
|
||||
self.door_connections[door["OriginalDoor"]] = door["Id"]
|
||||
|
||||
parent_region: Region = self.get_room_from_door(door["Id"])
|
||||
target_region: Region = self.get_room_from_door(door["OriginalDoor"])
|
||||
parent_region.add_exits({
|
||||
target_region.name: door["Id"]
|
||||
}, {
|
||||
target_region.name: lambda x: door.get("VisibilityFlags") != 1
|
||||
})
|
||||
|
||||
for index, loc in enumerate(location_table):
|
||||
if not world.boots_of_pleading[player] and loc["name"] == "BotSS: 2nd meeting with Redento":
|
||||
continue
|
||||
if not world.purified_hand[player] and loc["name"] == "MoM: Western room ledge":
|
||||
continue
|
||||
|
||||
region: Region = world.get_region(loc["room"], player)
|
||||
region.add_locations({loc["name"]: base_id + index})
|
||||
#id = base_id + location_table.index(loc)
|
||||
#reg.locations.append(BlasphemousLocation(player, loc["name"], id, reg))
|
||||
|
||||
for e, r in event_table.items():
|
||||
region: Region = world.get_region(r, player)
|
||||
event = BlasphemousLocation(player, e, None, region)
|
||||
event.show_in_spoiler = False
|
||||
event.place_locked_item(self.create_event(e))
|
||||
region.locations.append(event)
|
||||
|
||||
for door in door_table:
|
||||
region: Region = self.get_room_from_door(self.door_connections[door["Id"]])
|
||||
event = BlasphemousLocation(player, door["Id"], None, region)
|
||||
event.show_in_spoiler = False
|
||||
event.place_locked_item(self.create_event(door["Id"]))
|
||||
region.locations.append(event)
|
||||
|
||||
victory = Location(player, "His Holiness Escribar", None, world.get_region("D07Z01S03", player))
|
||||
victory = Location(player, "His Holiness Escribar", None, self.get_region("D07Z01S03[W]"))
|
||||
victory.place_locked_item(self.create_event("Victory"))
|
||||
world.get_region("D07Z01S03", player).locations.append(victory)
|
||||
self.get_region("D07Z01S03[W]").locations.append(victory)
|
||||
|
||||
if world.ending[self.player].value == 1:
|
||||
if self.options.ending == "ending_a":
|
||||
set_rule(victory, lambda state: state.has("Thorn Upgrade", player, 8))
|
||||
elif world.ending[self.player].value == 2:
|
||||
elif self.options.ending == "ending_c":
|
||||
set_rule(victory, lambda state: state.has("Thorn Upgrade", player, 8) and
|
||||
state.has("Holy Wound of Abnegation", player))
|
||||
|
||||
world.completion_condition[self.player] = lambda state: state.has("Victory", player)
|
||||
|
||||
|
||||
def get_room_from_door(self, door: str) -> Region:
|
||||
return self.multiworld.get_region(door.split("[")[0], self.player)
|
||||
|
||||
|
||||
def get_connected_door(self, door: str) -> Entrance:
|
||||
return self.multiworld.get_entrance(self.door_connections[door], self.player)
|
||||
multiworld.completion_condition[self.player] = lambda state: state.has("Victory", player)
|
||||
|
||||
|
||||
def fill_slot_data(self) -> Dict[str, Any]:
|
||||
slot_data: Dict[str, Any] = {}
|
||||
locations = []
|
||||
doors: Dict[str, str] = {}
|
||||
|
||||
world = self.multiworld
|
||||
player = self.player
|
||||
thorns: bool = True
|
||||
|
||||
if world.thorn_shuffle[player].value == 2:
|
||||
if self.options.thorn_shuffle == "vanilla":
|
||||
thorns = False
|
||||
|
||||
for loc in world.get_filled_locations(player):
|
||||
if loc.item.code == None:
|
||||
continue
|
||||
else:
|
||||
data = {
|
||||
"id": self.location_name_to_game_id[loc.name],
|
||||
"ap_id": loc.address,
|
||||
"name": loc.item.name,
|
||||
"player_name": world.player_name[loc.item.player],
|
||||
"type": int(loc.item.classification)
|
||||
}
|
||||
|
||||
locations.append(data)
|
||||
|
||||
config = {
|
||||
"LogicDifficulty": world.difficulty[player].value,
|
||||
"StartingLocation": world.starting_location[player].value,
|
||||
"LogicDifficulty": self.options.difficulty.value,
|
||||
"StartingLocation": self.options.starting_location.value,
|
||||
"VersionCreated": "AP",
|
||||
|
||||
"UnlockTeleportation": bool(world.prie_dieu_warp[player].value),
|
||||
"AllowHints": bool(world.corpse_hints[player].value),
|
||||
"AllowPenitence": bool(world.penitence[player].value),
|
||||
"UnlockTeleportation": bool(self.options.prie_dieu_warp.value),
|
||||
"AllowHints": bool(self.options.corpse_hints.value),
|
||||
"AllowPenitence": bool(self.options.penitence.value),
|
||||
|
||||
"ShuffleReliquaries": bool(world.reliquary_shuffle[player].value),
|
||||
"ShuffleBootsOfPleading": bool(world.boots_of_pleading[player].value),
|
||||
"ShufflePurifiedHand": bool(world.purified_hand[player].value),
|
||||
"ShuffleDash": bool(world.dash_shuffle[player].value),
|
||||
"ShuffleWallClimb": bool(world.wall_climb_shuffle[player].value),
|
||||
"ShuffleReliquaries": bool(self.options.reliquary_shuffle.value),
|
||||
"ShuffleBootsOfPleading": bool(self.options.boots_of_pleading.value),
|
||||
"ShufflePurifiedHand": bool(self.options.purified_hand.value),
|
||||
"ShuffleDash": bool(self.options.dash_shuffle.value),
|
||||
"ShuffleWallClimb": bool(self.options.wall_climb_shuffle.value),
|
||||
|
||||
"ShuffleSwordSkills": bool(world.skill_randomizer[player].value),
|
||||
"ShuffleSwordSkills": bool(self.options.wall_climb_shuffle.value),
|
||||
"ShuffleThorns": thorns,
|
||||
"JunkLongQuests": bool(world.skip_long_quests[player].value),
|
||||
"StartWithWheel": bool(world.start_wheel[player].value),
|
||||
"JunkLongQuests": bool(self.options.skip_long_quests.value),
|
||||
"StartWithWheel": bool(self.options.start_wheel.value),
|
||||
|
||||
"EnemyShuffleType": world.enemy_randomizer[player].value,
|
||||
"MaintainClass": bool(world.enemy_groups[player].value),
|
||||
"AreaScaling": bool(world.enemy_scaling[player].value),
|
||||
"EnemyShuffleType": self.options.enemy_randomizer.value,
|
||||
"MaintainClass": bool(self.options.enemy_groups.value),
|
||||
"AreaScaling": bool(self.options.enemy_scaling.value),
|
||||
|
||||
"BossShuffleType": 0,
|
||||
"DoorShuffleType": 0
|
||||
}
|
||||
|
||||
slot_data = {
|
||||
"locations": locations,
|
||||
"locationinfo": [{"gameId": loc, "apId": (base_id + index)} for index, loc in enumerate(location_names)],
|
||||
"doors": doors,
|
||||
"cfg": config,
|
||||
"ending": world.ending[self.player].value,
|
||||
"death_link": bool(world.death_link[self.player].value)
|
||||
"ending": self.options.ending.value,
|
||||
"death_link": bool(self.options.death_link.value)
|
||||
}
|
||||
|
||||
return slot_data
|
||||
|
||||
@@ -1,48 +1,17 @@
|
||||
# Blasphemous Multiworld Setup Guide
|
||||
|
||||
## Useful Links
|
||||
It is recommended to use the [Mod Installer](https://github.com/BrandenEK/Blasphemous.Modding.Installer) to handle installing and updating mods. If you would prefer to install mods manually, instructions can also be found at the Mod Installer repository.
|
||||
|
||||
Required:
|
||||
- Blasphemous: [Steam](https://store.steampowered.com/app/774361/Blasphemous/)
|
||||
- The GOG version of Blasphemous will also work.
|
||||
- Blasphemous Mod Installer: [GitHub](https://github.com/BrandenEK/Blasphemous-Mod-Installer)
|
||||
- Blasphemous Modding API: [GitHub](https://github.com/BrandenEK/Blasphemous-Modding-API)
|
||||
- Blasphemous Randomizer: [GitHub](https://github.com/BrandenEK/Blasphemous-Randomizer)
|
||||
- Blasphemous Multiworld: [GitHub](https://github.com/BrandenEK/Blasphemous-Multiworld)
|
||||
You will need the [Multiworld](https://github.com/BrandenEK/Blasphemous.Randomizer.Multiworld) mod to play an Archipelago randomizer.
|
||||
|
||||
Optional:
|
||||
- In-game map tracker: [GitHub](https://github.com/BrandenEK/Blasphemous-Rando-Map)
|
||||
- Quick Prie Dieu warp mod: [GitHub](https://github.com/BadMagic100/Blasphemous-PrieWarp)
|
||||
- Boots of Pleading mod: [GitHub](https://github.com/BrandenEK/Blasphemous-Boots-of-Pleading)
|
||||
- Double Jump mod: [GitHub](https://github.com/BrandenEK/Blasphemous-Double-Jump)
|
||||
Some optional mods are also recommended:
|
||||
- [Rando Map](https://github.com/BrandenEK/Blasphemous.Randomizer.MapTracker)
|
||||
- [Boots of Pleading](https://github.com/BrandenEK/Blasphemous.BootsOfPleading) (Required if the "Boots of Pleading" option is enabled)
|
||||
- [Double Jump](https://github.com/BrandenEK/Blasphemous.DoubleJump) (Required if the "Purified Hand of the Nun" option is enabled)
|
||||
|
||||
## Mod Installer (Recommended)
|
||||
To connect to a multiworld: Choose a save file and enter the address, your name, and the password (if the server has one) into the menu.
|
||||
|
||||
1. Download the [Mod Installer](https://github.com/BrandenEK/Blasphemous-Mod-Installer),
|
||||
and point it to your install directory for Blasphemous.
|
||||
|
||||
2. Install the `Modding API`, `Randomizer`, and `Multiworld` mods. Optionally, you can also install the
|
||||
`Rando Map`, `PrieWarp`, `Boots of Pleading`, and `Double Jump` mods, and set up the PopTracker pack if desired.
|
||||
|
||||
3. Start Blasphemous. To verfy that the mods are working, look for a version number for both
|
||||
the Randomizer and Multiworld on the title screen.
|
||||
|
||||
## Manual Installation
|
||||
|
||||
1. Download the [Modding API](https://github.com/BrandenEK/Blasphemous-Modding-API/releases), and follow
|
||||
the [installation instructions](https://github.com/BrandenEK/Blasphemous-Modding-API#installation) on the GitHub page.
|
||||
|
||||
2. After the Modding API has been installed, download the
|
||||
[Randomizer](https://github.com/BrandenEK/Blasphemous-Randomizer/releases) and
|
||||
[Multiworld](https://github.com/BrandenEK/Blasphemous-Multiworld/releases) archives, and extract the contents of both
|
||||
into the `Modding` folder. Then, add any desired additional mods.
|
||||
|
||||
3. Start Blasphemous. To verfy that the mods are working, look for a version number for both
|
||||
the Randomizer and Multiworld on the title screen.
|
||||
|
||||
## Connecting
|
||||
|
||||
To connect to an Archipelago server, open the in-game console by pressing backslash `\` and use
|
||||
the command `multiworld connect [address:port] [name] [password]`.
|
||||
The port and password are both optional - if no port is provided then the default port of 38281 is used.
|
||||
**Make sure to connect to the server before attempting to start a new save file.**
|
||||
After connecting, there are some commands you can use in the console, which can be opened by pressing backslash `\`:
|
||||
- `ap status` - Display connection status.
|
||||
- `ap say [message]` - Send a message to the server.
|
||||
- `ap hint [item]` - Request a hint for an item from the server.
|
||||
48070
worlds/blasphemous/region_data.py
generated
Normal file
48070
worlds/blasphemous/region_data.py
generated
Normal file
File diff suppressed because it is too large
Load Diff
7
worlds/blasphemous/test/__init__.py
Normal file
7
worlds/blasphemous/test/__init__.py
Normal file
@@ -0,0 +1,7 @@
|
||||
from test.bases import WorldTestBase
|
||||
from .. import BlasphemousWorld
|
||||
|
||||
|
||||
class BlasphemousTestBase(WorldTestBase):
|
||||
game = "Blasphemous"
|
||||
world: BlasphemousWorld
|
||||
56
worlds/blasphemous/test/test_background_zones.py
Normal file
56
worlds/blasphemous/test/test_background_zones.py
Normal file
@@ -0,0 +1,56 @@
|
||||
from . import BlasphemousTestBase
|
||||
from ..Locations import location_names
|
||||
|
||||
|
||||
class BotSSGauntletTest(BlasphemousTestBase):
|
||||
options = {
|
||||
"starting_location": "albero",
|
||||
"wall_climb_shuffle": True,
|
||||
"dash_shuffle": True
|
||||
}
|
||||
|
||||
@property
|
||||
def run_default_tests(self) -> bool:
|
||||
return False
|
||||
|
||||
def test_botss_gauntlet(self) -> None:
|
||||
self.assertAccessDependency([location_names["CO25"]], [["Dash Ability", "Wall Climb Ability"]], True)
|
||||
|
||||
|
||||
class BackgroundZonesTest(BlasphemousTestBase):
|
||||
@property
|
||||
def run_default_tests(self) -> bool:
|
||||
return False
|
||||
|
||||
def test_dc_shroud(self) -> None:
|
||||
self.assertAccessDependency([location_names["RB03"]], [["Shroud of Dreamt Sins"]], True)
|
||||
|
||||
def test_wothp_bronze_cells(self) -> None:
|
||||
bronze_locations = [
|
||||
location_names["QI70"],
|
||||
location_names["RESCUED_CHERUB_03"]
|
||||
]
|
||||
|
||||
self.assertAccessDependency(bronze_locations, [["Key of the Secular"]], True)
|
||||
|
||||
def test_wothp_silver_cells(self) -> None:
|
||||
silver_locations = [
|
||||
location_names["CO24"],
|
||||
location_names["RESCUED_CHERUB_34"],
|
||||
location_names["CO37"],
|
||||
location_names["RESCUED_CHERUB_04"]
|
||||
]
|
||||
|
||||
self.assertAccessDependency(silver_locations, [["Key of the Scribe"]], True)
|
||||
|
||||
def test_wothp_gold_cells(self) -> None:
|
||||
gold_locations = [
|
||||
location_names["QI51"],
|
||||
location_names["CO26"],
|
||||
location_names["CO02"]
|
||||
]
|
||||
|
||||
self.assertAccessDependency(gold_locations, [["Key of the Inquisitor"]], True)
|
||||
|
||||
def test_wothp_quirce(self) -> None:
|
||||
self.assertAccessDependency([location_names["BS14"]], [["Key of the Secular", "Key of the Scribe", "Key of the Inquisitor"]], True)
|
||||
135
worlds/blasphemous/test/test_starting_locations.py
Normal file
135
worlds/blasphemous/test/test_starting_locations.py
Normal file
@@ -0,0 +1,135 @@
|
||||
from . import BlasphemousTestBase
|
||||
|
||||
|
||||
class TestBrotherhoodEasy(BlasphemousTestBase):
|
||||
options = {
|
||||
"starting_location": "brotherhood",
|
||||
"difficulty": "easy"
|
||||
}
|
||||
|
||||
|
||||
class TestBrotherhoodNormal(BlasphemousTestBase):
|
||||
options = {
|
||||
"starting_location": "brotherhood",
|
||||
"difficulty": "normal"
|
||||
}
|
||||
|
||||
|
||||
class TestBrotherhoodHard(BlasphemousTestBase):
|
||||
options = {
|
||||
"starting_location": "brotherhood",
|
||||
"difficulty": "hard"
|
||||
}
|
||||
|
||||
|
||||
class TestAlberoEasy(BlasphemousTestBase):
|
||||
options = {
|
||||
"starting_location": "albero",
|
||||
"difficulty": "easy"
|
||||
}
|
||||
|
||||
|
||||
class TestAlberoNormal(BlasphemousTestBase):
|
||||
options = {
|
||||
"starting_location": "albero",
|
||||
"difficulty": "normal"
|
||||
}
|
||||
|
||||
|
||||
class TestAlberoHard(BlasphemousTestBase):
|
||||
options = {
|
||||
"starting_location": "albero",
|
||||
"difficulty": "hard"
|
||||
}
|
||||
|
||||
|
||||
class TestConventEasy(BlasphemousTestBase):
|
||||
options = {
|
||||
"starting_location": "convent",
|
||||
"difficulty": "easy"
|
||||
}
|
||||
|
||||
|
||||
class TestConventNormal(BlasphemousTestBase):
|
||||
options = {
|
||||
"starting_location": "convent",
|
||||
"difficulty": "normal"
|
||||
}
|
||||
|
||||
|
||||
class TestConventHard(BlasphemousTestBase):
|
||||
options = {
|
||||
"starting_location": "convent",
|
||||
"difficulty": "hard"
|
||||
}
|
||||
|
||||
|
||||
class TestGrievanceEasy(BlasphemousTestBase):
|
||||
options = {
|
||||
"starting_location": "grievance",
|
||||
"difficulty": "easy"
|
||||
}
|
||||
|
||||
|
||||
class TestGrievanceNormal(BlasphemousTestBase):
|
||||
options = {
|
||||
"starting_location": "grievance",
|
||||
"difficulty": "normal"
|
||||
}
|
||||
|
||||
|
||||
class TestGrievanceHard(BlasphemousTestBase):
|
||||
options = {
|
||||
"starting_location": "grievance",
|
||||
"difficulty": "hard"
|
||||
}
|
||||
|
||||
|
||||
class TestKnotOfWordsEasy(BlasphemousTestBase):
|
||||
options = {
|
||||
"starting_location": "knot_of_words",
|
||||
"difficulty": "easy"
|
||||
}
|
||||
|
||||
|
||||
class TestKnotOfWordsNormal(BlasphemousTestBase):
|
||||
options = {
|
||||
"starting_location": "knot_of_words",
|
||||
"difficulty": "normal"
|
||||
}
|
||||
|
||||
|
||||
class TestKnotOfWordsHard(BlasphemousTestBase):
|
||||
options = {
|
||||
"starting_location": "knot_of_words",
|
||||
"difficulty": "hard"
|
||||
}
|
||||
|
||||
|
||||
class TestRooftopsEasy(BlasphemousTestBase):
|
||||
options = {
|
||||
"starting_location": "rooftops",
|
||||
"difficulty": "easy"
|
||||
}
|
||||
|
||||
|
||||
class TestRooftopsNormal(BlasphemousTestBase):
|
||||
options = {
|
||||
"starting_location": "rooftops",
|
||||
"difficulty": "normal"
|
||||
}
|
||||
|
||||
|
||||
class TestRooftopsHard(BlasphemousTestBase):
|
||||
options = {
|
||||
"starting_location": "rooftops",
|
||||
"difficulty": "hard"
|
||||
}
|
||||
|
||||
|
||||
# mourning and havoc can't be selected on easy or normal. hard only
|
||||
class TestMourningHavocHard(BlasphemousTestBase):
|
||||
options = {
|
||||
"starting_location": "mourning_havoc",
|
||||
"difficulty": "hard"
|
||||
}
|
||||
@@ -28,7 +28,7 @@ An Example `AP.json` file:
|
||||
|
||||
```
|
||||
{
|
||||
"Url": "archipelago:12345",
|
||||
"Url": "archipelago.gg:12345",
|
||||
"SlotName": "Maddy",
|
||||
"Password": ""
|
||||
}
|
||||
|
||||
@@ -44,15 +44,15 @@ class ChecksFinderWorld(World):
|
||||
self.multiworld.regions += [menu, board]
|
||||
|
||||
def create_items(self):
|
||||
# Generate item pool
|
||||
itempool = []
|
||||
# Generate list of items
|
||||
items_to_create = []
|
||||
# Add the map width and height stuff
|
||||
itempool += ["Map Width"] * 5 # 10 - 5
|
||||
itempool += ["Map Height"] * 5 # 10 - 5
|
||||
items_to_create += ["Map Width"] * 5 # 10 - 5
|
||||
items_to_create += ["Map Height"] * 5 # 10 - 5
|
||||
# Add the map bombs
|
||||
itempool += ["Map Bombs"] * 15 # 20 - 5
|
||||
# Convert itempool into real items
|
||||
itempool = [self.create_item(item) for item in itempool]
|
||||
items_to_create += ["Map Bombs"] * 15 # 20 - 5
|
||||
# Convert list into real items
|
||||
itempool = [self.create_item(item) for item in items_to_create]
|
||||
|
||||
self.multiworld.itempool += itempool
|
||||
|
||||
|
||||
258
worlds/kh1/Client.py
Normal file
258
worlds/kh1/Client.py
Normal file
@@ -0,0 +1,258 @@
|
||||
from __future__ import annotations
|
||||
import os
|
||||
import json
|
||||
import sys
|
||||
import asyncio
|
||||
import shutil
|
||||
import logging
|
||||
import re
|
||||
import time
|
||||
from calendar import timegm
|
||||
|
||||
import ModuleUpdate
|
||||
ModuleUpdate.update()
|
||||
|
||||
import Utils
|
||||
death_link = False
|
||||
item_num = 1
|
||||
|
||||
logger = logging.getLogger("Client")
|
||||
|
||||
if __name__ == "__main__":
|
||||
Utils.init_logging("KH1Client", exception_logger="Client")
|
||||
|
||||
from NetUtils import NetworkItem, ClientStatus
|
||||
from CommonClient import gui_enabled, logger, get_base_parser, ClientCommandProcessor, \
|
||||
CommonContext, server_loop
|
||||
|
||||
|
||||
def check_stdin() -> None:
|
||||
if Utils.is_windows and sys.stdin:
|
||||
print("WARNING: Console input is not routed reliably on Windows, use the GUI instead.")
|
||||
|
||||
class KH1ClientCommandProcessor(ClientCommandProcessor):
|
||||
def _cmd_deathlink(self):
|
||||
"""Toggles Deathlink"""
|
||||
global death_link
|
||||
if death_link:
|
||||
death_link = False
|
||||
self.output(f"Death Link turned off")
|
||||
else:
|
||||
death_link = True
|
||||
self.output(f"Death Link turned on")
|
||||
|
||||
class KH1Context(CommonContext):
|
||||
command_processor: int = KH1ClientCommandProcessor
|
||||
game = "Kingdom Hearts"
|
||||
items_handling = 0b111 # full remote
|
||||
|
||||
def __init__(self, server_address, password):
|
||||
super(KH1Context, self).__init__(server_address, password)
|
||||
self.send_index: int = 0
|
||||
self.syncing = False
|
||||
self.awaiting_bridge = False
|
||||
# self.game_communication_path: files go in this path to pass data between us and the actual game
|
||||
if "localappdata" in os.environ:
|
||||
self.game_communication_path = os.path.expandvars(r"%localappdata%/KH1FM")
|
||||
else:
|
||||
self.game_communication_path = os.path.expandvars(r"$HOME/KH1FM")
|
||||
if not os.path.exists(self.game_communication_path):
|
||||
os.makedirs(self.game_communication_path)
|
||||
for root, dirs, files in os.walk(self.game_communication_path):
|
||||
for file in files:
|
||||
if file.find("obtain") <= -1:
|
||||
os.remove(root+"/"+file)
|
||||
|
||||
async def server_auth(self, password_requested: bool = False):
|
||||
if password_requested and not self.password:
|
||||
await super(KH1Context, self).server_auth(password_requested)
|
||||
await self.get_username()
|
||||
await self.send_connect()
|
||||
|
||||
async def connection_closed(self):
|
||||
await super(KH1Context, self).connection_closed()
|
||||
for root, dirs, files in os.walk(self.game_communication_path):
|
||||
for file in files:
|
||||
if file.find("obtain") <= -1:
|
||||
os.remove(root + "/" + file)
|
||||
global item_num
|
||||
item_num = 1
|
||||
|
||||
@property
|
||||
def endpoints(self):
|
||||
if self.server:
|
||||
return [self.server]
|
||||
else:
|
||||
return []
|
||||
|
||||
async def shutdown(self):
|
||||
await super(KH1Context, self).shutdown()
|
||||
for root, dirs, files in os.walk(self.game_communication_path):
|
||||
for file in files:
|
||||
if file.find("obtain") <= -1:
|
||||
os.remove(root+"/"+file)
|
||||
global item_num
|
||||
item_num = 1
|
||||
|
||||
def on_package(self, cmd: str, args: dict):
|
||||
if cmd in {"Connected"}:
|
||||
if not os.path.exists(self.game_communication_path):
|
||||
os.makedirs(self.game_communication_path)
|
||||
for ss in self.checked_locations:
|
||||
filename = f"send{ss}"
|
||||
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
|
||||
f.close()
|
||||
|
||||
#Handle Slot Data
|
||||
for key in list(args['slot_data'].keys()):
|
||||
with open(os.path.join(self.game_communication_path, key + ".cfg"), 'w') as f:
|
||||
f.write(str(args['slot_data'][key]))
|
||||
f.close()
|
||||
|
||||
###Support Legacy Games
|
||||
if "Required Reports" in list(args['slot_data'].keys()) and "required_reports_eotw" not in list(args['slot_data'].keys()):
|
||||
reports_required = args['slot_data']["Required Reports"]
|
||||
with open(os.path.join(self.game_communication_path, "required_reports.cfg"), 'w') as f:
|
||||
f.write(str(reports_required))
|
||||
f.close()
|
||||
###End Support Legacy Games
|
||||
|
||||
#End Handle Slot Data
|
||||
|
||||
if cmd in {"ReceivedItems"}:
|
||||
start_index = args["index"]
|
||||
if start_index != len(self.items_received):
|
||||
global item_num
|
||||
for item in args['items']:
|
||||
found = False
|
||||
item_filename = f"AP_{str(item_num)}.item"
|
||||
for filename in os.listdir(self.game_communication_path):
|
||||
if filename == item_filename:
|
||||
found = True
|
||||
if not found:
|
||||
with open(os.path.join(self.game_communication_path, item_filename), 'w') as f:
|
||||
f.write(str(NetworkItem(*item).item) + "\n" + str(NetworkItem(*item).location) + "\n" + str(NetworkItem(*item).player))
|
||||
f.close()
|
||||
item_num = item_num + 1
|
||||
|
||||
if cmd in {"RoomUpdate"}:
|
||||
if "checked_locations" in args:
|
||||
for ss in self.checked_locations:
|
||||
filename = f"send{ss}"
|
||||
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
|
||||
f.close()
|
||||
|
||||
if cmd in {"PrintJSON"} and "type" in args:
|
||||
if args["type"] == "ItemSend":
|
||||
item = args["item"]
|
||||
networkItem = NetworkItem(*item)
|
||||
recieverID = args["receiving"]
|
||||
senderID = networkItem.player
|
||||
locationID = networkItem.location
|
||||
if recieverID != self.slot and senderID == self.slot:
|
||||
itemName = self.item_names.lookup_in_slot(networkItem.item, recieverID)
|
||||
itemCategory = networkItem.flags
|
||||
recieverName = self.player_names[recieverID]
|
||||
filename = "sent"
|
||||
with open(os.path.join(self.game_communication_path, filename), 'w') as f:
|
||||
f.write(
|
||||
re.sub('[^A-Za-z0-9 ]+', '',str(itemName))[:15] + "\n"
|
||||
+ re.sub('[^A-Za-z0-9 ]+', '',str(recieverName))[:6] + "\n"
|
||||
+ str(itemCategory) + "\n"
|
||||
+ str(locationID))
|
||||
f.close()
|
||||
|
||||
def on_deathlink(self, data: dict[str, object]):
|
||||
self.last_death_link = max(data["time"], self.last_death_link)
|
||||
text = data.get("cause", "")
|
||||
if text:
|
||||
logger.info(f"DeathLink: {text}")
|
||||
else:
|
||||
logger.info(f"DeathLink: Received from {data['source']}")
|
||||
with open(os.path.join(self.game_communication_path, 'dlreceive'), 'w') as f:
|
||||
f.write(str(int(data["time"])))
|
||||
f.close()
|
||||
|
||||
def run_gui(self):
|
||||
"""Import kivy UI system and start running it as self.ui_task."""
|
||||
from kvui import GameManager
|
||||
|
||||
class KH1Manager(GameManager):
|
||||
logging_pairs = [
|
||||
("Client", "Archipelago")
|
||||
]
|
||||
base_title = "Archipelago KH1 Client"
|
||||
|
||||
self.ui = KH1Manager(self)
|
||||
self.ui_task = asyncio.create_task(self.ui.async_run(), name="UI")
|
||||
|
||||
|
||||
async def game_watcher(ctx: KH1Context):
|
||||
from .Locations import lookup_id_to_name
|
||||
while not ctx.exit_event.is_set():
|
||||
global death_link
|
||||
if death_link and "DeathLink" not in ctx.tags:
|
||||
await ctx.update_death_link(death_link)
|
||||
if not death_link and "DeathLink" in ctx.tags:
|
||||
await ctx.update_death_link(death_link)
|
||||
if ctx.syncing == True:
|
||||
sync_msg = [{'cmd': 'Sync'}]
|
||||
if ctx.locations_checked:
|
||||
sync_msg.append({"cmd": "LocationChecks", "locations": list(ctx.locations_checked)})
|
||||
await ctx.send_msgs(sync_msg)
|
||||
ctx.syncing = False
|
||||
sending = []
|
||||
victory = False
|
||||
for root, dirs, files in os.walk(ctx.game_communication_path):
|
||||
for file in files:
|
||||
if file.find("send") > -1:
|
||||
st = file.split("send", -1)[1]
|
||||
if st != "nil":
|
||||
sending = sending+[(int(st))]
|
||||
if file.find("victory") > -1:
|
||||
victory = True
|
||||
if file.find("dlsend") > -1 and "DeathLink" in ctx.tags:
|
||||
st = file.split("dlsend", -1)[1]
|
||||
if st != "nil":
|
||||
if timegm(time.strptime(st, '%Y%m%d%H%M%S')) > ctx.last_death_link and int(time.time()) % int(timegm(time.strptime(st, '%Y%m%d%H%M%S'))) < 10:
|
||||
await ctx.send_death(death_text = "Sora was defeated!")
|
||||
if file.find("insynthshop") > -1:
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "LocationScouts",
|
||||
"locations": [2656401,2656402,2656403,2656404,2656405,2656406],
|
||||
"create_as_hint": 2
|
||||
}])
|
||||
ctx.locations_checked = sending
|
||||
message = [{"cmd": 'LocationChecks', "locations": sending}]
|
||||
await ctx.send_msgs(message)
|
||||
if not ctx.finished_game and victory:
|
||||
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
||||
ctx.finished_game = True
|
||||
await asyncio.sleep(0.1)
|
||||
|
||||
|
||||
def launch():
|
||||
async def main(args):
|
||||
ctx = KH1Context(args.connect, args.password)
|
||||
ctx.server_task = asyncio.create_task(server_loop(ctx), name="server loop")
|
||||
if gui_enabled:
|
||||
ctx.run_gui()
|
||||
ctx.run_cli()
|
||||
progression_watcher = asyncio.create_task(
|
||||
game_watcher(ctx), name="KH1ProgressionWatcher")
|
||||
|
||||
await ctx.exit_event.wait()
|
||||
ctx.server_address = None
|
||||
|
||||
await progression_watcher
|
||||
|
||||
await ctx.shutdown()
|
||||
|
||||
import colorama
|
||||
|
||||
parser = get_base_parser(description="KH1 Client, for text interfacing.")
|
||||
|
||||
args, rest = parser.parse_known_args()
|
||||
colorama.init()
|
||||
asyncio.run(main(args))
|
||||
colorama.deinit()
|
||||
532
worlds/kh1/Items.py
Normal file
532
worlds/kh1/Items.py
Normal file
@@ -0,0 +1,532 @@
|
||||
from typing import Dict, NamedTuple, Optional, Set
|
||||
|
||||
from BaseClasses import Item, ItemClassification
|
||||
|
||||
|
||||
class KH1Item(Item):
|
||||
game: str = "Kingdom Hearts"
|
||||
|
||||
|
||||
class KH1ItemData(NamedTuple):
|
||||
category: str
|
||||
code: int
|
||||
classification: ItemClassification = ItemClassification.filler
|
||||
max_quantity: int = 1
|
||||
weight: int = 1
|
||||
|
||||
|
||||
def get_items_by_category(category: str) -> Dict[str, KH1ItemData]:
|
||||
item_dict: Dict[str, KH1ItemData] = {}
|
||||
for name, data in item_table.items():
|
||||
if data.category == category:
|
||||
item_dict.setdefault(name, data)
|
||||
|
||||
return item_dict
|
||||
|
||||
|
||||
item_table: Dict[str, KH1ItemData] = {
|
||||
"Victory": KH1ItemData("VIC", code = 264_0000, classification = ItemClassification.progression, ),
|
||||
"Potion": KH1ItemData("Item", code = 264_1001, classification = ItemClassification.filler, ),
|
||||
"Hi-Potion": KH1ItemData("Item", code = 264_1002, classification = ItemClassification.filler, ),
|
||||
"Ether": KH1ItemData("Item", code = 264_1003, classification = ItemClassification.filler, ),
|
||||
"Elixir": KH1ItemData("Item", code = 264_1004, classification = ItemClassification.filler, ),
|
||||
#"B05": KH1ItemData("Item", code = 264_1005, classification = ItemClassification.filler, ),
|
||||
"Mega-Potion": KH1ItemData("Item", code = 264_1006, classification = ItemClassification.filler, ),
|
||||
"Mega-Ether": KH1ItemData("Item", code = 264_1007, classification = ItemClassification.filler, ),
|
||||
"Megalixir": KH1ItemData("Item", code = 264_1008, classification = ItemClassification.filler, ),
|
||||
#"Fury Stone": KH1ItemData("Synthesis", code = 264_1009, classification = ItemClassification.filler, ),
|
||||
#"Power Stone": KH1ItemData("Synthesis", code = 264_1010, classification = ItemClassification.filler, ),
|
||||
#"Energy Stone": KH1ItemData("Synthesis", code = 264_1011, classification = ItemClassification.filler, ),
|
||||
#"Blazing Stone": KH1ItemData("Synthesis", code = 264_1012, classification = ItemClassification.filler, ),
|
||||
#"Frost Stone": KH1ItemData("Synthesis", code = 264_1013, classification = ItemClassification.filler, ),
|
||||
#"Lightning Stone": KH1ItemData("Synthesis", code = 264_1014, classification = ItemClassification.filler, ),
|
||||
#"Dazzling Stone": KH1ItemData("Synthesis", code = 264_1015, classification = ItemClassification.filler, ),
|
||||
#"Stormy Stone": KH1ItemData("Synthesis", code = 264_1016, classification = ItemClassification.filler, ),
|
||||
"Protect Chain": KH1ItemData("Accessory", code = 264_1017, classification = ItemClassification.useful, ),
|
||||
"Protera Chain": KH1ItemData("Accessory", code = 264_1018, classification = ItemClassification.useful, ),
|
||||
"Protega Chain": KH1ItemData("Accessory", code = 264_1019, classification = ItemClassification.useful, ),
|
||||
"Fire Ring": KH1ItemData("Accessory", code = 264_1020, classification = ItemClassification.useful, ),
|
||||
"Fira Ring": KH1ItemData("Accessory", code = 264_1021, classification = ItemClassification.useful, ),
|
||||
"Firaga Ring": KH1ItemData("Accessory", code = 264_1022, classification = ItemClassification.useful, ),
|
||||
"Blizzard Ring": KH1ItemData("Accessory", code = 264_1023, classification = ItemClassification.useful, ),
|
||||
"Blizzara Ring": KH1ItemData("Accessory", code = 264_1024, classification = ItemClassification.useful, ),
|
||||
"Blizzaga Ring": KH1ItemData("Accessory", code = 264_1025, classification = ItemClassification.useful, ),
|
||||
"Thunder Ring": KH1ItemData("Accessory", code = 264_1026, classification = ItemClassification.useful, ),
|
||||
"Thundara Ring": KH1ItemData("Accessory", code = 264_1027, classification = ItemClassification.useful, ),
|
||||
"Thundaga Ring": KH1ItemData("Accessory", code = 264_1028, classification = ItemClassification.useful, ),
|
||||
"Ability Stud": KH1ItemData("Accessory", code = 264_1029, classification = ItemClassification.useful, ),
|
||||
"Guard Earring": KH1ItemData("Accessory", code = 264_1030, classification = ItemClassification.useful, ),
|
||||
"Master Earring": KH1ItemData("Accessory", code = 264_1031, classification = ItemClassification.useful, ),
|
||||
"Chaos Ring": KH1ItemData("Accessory", code = 264_1032, classification = ItemClassification.useful, ),
|
||||
"Dark Ring": KH1ItemData("Accessory", code = 264_1033, classification = ItemClassification.useful, ),
|
||||
"Element Ring": KH1ItemData("Accessory", code = 264_1034, classification = ItemClassification.useful, ),
|
||||
"Three Stars": KH1ItemData("Accessory", code = 264_1035, classification = ItemClassification.useful, ),
|
||||
"Power Chain": KH1ItemData("Accessory", code = 264_1036, classification = ItemClassification.useful, ),
|
||||
"Golem Chain": KH1ItemData("Accessory", code = 264_1037, classification = ItemClassification.useful, ),
|
||||
"Titan Chain": KH1ItemData("Accessory", code = 264_1038, classification = ItemClassification.useful, ),
|
||||
"Energy Bangle": KH1ItemData("Accessory", code = 264_1039, classification = ItemClassification.useful, ),
|
||||
"Angel Bangle": KH1ItemData("Accessory", code = 264_1040, classification = ItemClassification.useful, ),
|
||||
"Gaia Bangle": KH1ItemData("Accessory", code = 264_1041, classification = ItemClassification.useful, ),
|
||||
"Magic Armlet": KH1ItemData("Accessory", code = 264_1042, classification = ItemClassification.useful, ),
|
||||
"Rune Armlet": KH1ItemData("Accessory", code = 264_1043, classification = ItemClassification.useful, ),
|
||||
"Atlas Armlet": KH1ItemData("Accessory", code = 264_1044, classification = ItemClassification.useful, ),
|
||||
"Heartguard": KH1ItemData("Accessory", code = 264_1045, classification = ItemClassification.useful, ),
|
||||
"Ribbon": KH1ItemData("Accessory", code = 264_1046, classification = ItemClassification.useful, ),
|
||||
"Crystal Crown": KH1ItemData("Accessory", code = 264_1047, classification = ItemClassification.useful, ),
|
||||
"Brave Warrior": KH1ItemData("Accessory", code = 264_1048, classification = ItemClassification.useful, ),
|
||||
"Ifrit's Horn": KH1ItemData("Accessory", code = 264_1049, classification = ItemClassification.useful, ),
|
||||
"Inferno Band": KH1ItemData("Accessory", code = 264_1050, classification = ItemClassification.useful, ),
|
||||
"White Fang": KH1ItemData("Accessory", code = 264_1051, classification = ItemClassification.useful, ),
|
||||
"Ray of Light": KH1ItemData("Accessory", code = 264_1052, classification = ItemClassification.useful, ),
|
||||
"Holy Circlet": KH1ItemData("Accessory", code = 264_1053, classification = ItemClassification.useful, ),
|
||||
"Raven's Claw": KH1ItemData("Accessory", code = 264_1054, classification = ItemClassification.useful, ),
|
||||
"Omega Arts": KH1ItemData("Accessory", code = 264_1055, classification = ItemClassification.useful, ),
|
||||
"EXP Earring": KH1ItemData("Accessory", code = 264_1056, classification = ItemClassification.useful, ),
|
||||
#"A41": KH1ItemData("Accessory", code = 264_1057, classification = ItemClassification.useful, ),
|
||||
"EXP Ring": KH1ItemData("Accessory", code = 264_1058, classification = ItemClassification.useful, ),
|
||||
"EXP Bracelet": KH1ItemData("Accessory", code = 264_1059, classification = ItemClassification.useful, ),
|
||||
"EXP Necklace": KH1ItemData("Accessory", code = 264_1060, classification = ItemClassification.useful, ),
|
||||
"Firagun Band": KH1ItemData("Accessory", code = 264_1061, classification = ItemClassification.useful, ),
|
||||
"Blizzagun Band": KH1ItemData("Accessory", code = 264_1062, classification = ItemClassification.useful, ),
|
||||
"Thundagun Band": KH1ItemData("Accessory", code = 264_1063, classification = ItemClassification.useful, ),
|
||||
"Ifrit Belt": KH1ItemData("Accessory", code = 264_1064, classification = ItemClassification.useful, ),
|
||||
"Shiva Belt": KH1ItemData("Accessory", code = 264_1065, classification = ItemClassification.useful, ),
|
||||
"Ramuh Belt": KH1ItemData("Accessory", code = 264_1066, classification = ItemClassification.useful, ),
|
||||
"Moogle Badge": KH1ItemData("Accessory", code = 264_1067, classification = ItemClassification.useful, ),
|
||||
"Cosmic Arts": KH1ItemData("Accessory", code = 264_1068, classification = ItemClassification.useful, ),
|
||||
"Royal Crown": KH1ItemData("Accessory", code = 264_1069, classification = ItemClassification.useful, ),
|
||||
"Prime Cap": KH1ItemData("Accessory", code = 264_1070, classification = ItemClassification.useful, ),
|
||||
"Obsidian Ring": KH1ItemData("Accessory", code = 264_1071, classification = ItemClassification.useful, ),
|
||||
#"A56": KH1ItemData("Accessory", code = 264_1072, classification = ItemClassification.filler, ),
|
||||
#"A57": KH1ItemData("Accessory", code = 264_1073, classification = ItemClassification.filler, ),
|
||||
#"A58": KH1ItemData("Accessory", code = 264_1074, classification = ItemClassification.filler, ),
|
||||
#"A59": KH1ItemData("Accessory", code = 264_1075, classification = ItemClassification.filler, ),
|
||||
#"A60": KH1ItemData("Accessory", code = 264_1076, classification = ItemClassification.filler, ),
|
||||
#"A61": KH1ItemData("Accessory", code = 264_1077, classification = ItemClassification.filler, ),
|
||||
#"A62": KH1ItemData("Accessory", code = 264_1078, classification = ItemClassification.filler, ),
|
||||
#"A63": KH1ItemData("Accessory", code = 264_1079, classification = ItemClassification.filler, ),
|
||||
#"A64": KH1ItemData("Accessory", code = 264_1080, classification = ItemClassification.filler, ),
|
||||
#"Kingdom Key": KH1ItemData("Keyblades", code = 264_1081, classification = ItemClassification.useful, ),
|
||||
#"Dream Sword": KH1ItemData("Keyblades", code = 264_1082, classification = ItemClassification.useful, ),
|
||||
#"Dream Shield": KH1ItemData("Keyblades", code = 264_1083, classification = ItemClassification.useful, ),
|
||||
#"Dream Rod": KH1ItemData("Keyblades", code = 264_1084, classification = ItemClassification.useful, ),
|
||||
"Wooden Sword": KH1ItemData("Keyblades", code = 264_1085, classification = ItemClassification.useful, ),
|
||||
"Jungle King": KH1ItemData("Keyblades", code = 264_1086, classification = ItemClassification.progression, ),
|
||||
"Three Wishes": KH1ItemData("Keyblades", code = 264_1087, classification = ItemClassification.progression, ),
|
||||
"Fairy Harp": KH1ItemData("Keyblades", code = 264_1088, classification = ItemClassification.progression, ),
|
||||
"Pumpkinhead": KH1ItemData("Keyblades", code = 264_1089, classification = ItemClassification.progression, ),
|
||||
"Crabclaw": KH1ItemData("Keyblades", code = 264_1090, classification = ItemClassification.useful, ),
|
||||
"Divine Rose": KH1ItemData("Keyblades", code = 264_1091, classification = ItemClassification.progression, ),
|
||||
"Spellbinder": KH1ItemData("Keyblades", code = 264_1092, classification = ItemClassification.useful, ),
|
||||
"Olympia": KH1ItemData("Keyblades", code = 264_1093, classification = ItemClassification.progression, ),
|
||||
"Lionheart": KH1ItemData("Keyblades", code = 264_1094, classification = ItemClassification.progression, ),
|
||||
"Metal Chocobo": KH1ItemData("Keyblades", code = 264_1095, classification = ItemClassification.useful, ),
|
||||
"Oathkeeper": KH1ItemData("Keyblades", code = 264_1096, classification = ItemClassification.progression, ),
|
||||
"Oblivion": KH1ItemData("Keyblades", code = 264_1097, classification = ItemClassification.progression, ),
|
||||
"Lady Luck": KH1ItemData("Keyblades", code = 264_1098, classification = ItemClassification.progression, ),
|
||||
"Wishing Star": KH1ItemData("Keyblades", code = 264_1099, classification = ItemClassification.progression, ),
|
||||
"Ultima Weapon": KH1ItemData("Keyblades", code = 264_1100, classification = ItemClassification.useful, ),
|
||||
"Diamond Dust": KH1ItemData("Keyblades", code = 264_1101, classification = ItemClassification.useful, ),
|
||||
"One-Winged Angel": KH1ItemData("Keyblades", code = 264_1102, classification = ItemClassification.useful, ),
|
||||
#"Mage's Staff": KH1ItemData("Weapons", code = 264_1103, classification = ItemClassification.filler, ),
|
||||
"Morning Star": KH1ItemData("Weapons", code = 264_1104, classification = ItemClassification.useful, ),
|
||||
"Shooting Star": KH1ItemData("Weapons", code = 264_1105, classification = ItemClassification.useful, ),
|
||||
"Magus Staff": KH1ItemData("Weapons", code = 264_1106, classification = ItemClassification.useful, ),
|
||||
"Wisdom Staff": KH1ItemData("Weapons", code = 264_1107, classification = ItemClassification.useful, ),
|
||||
"Warhammer": KH1ItemData("Weapons", code = 264_1108, classification = ItemClassification.useful, ),
|
||||
"Silver Mallet": KH1ItemData("Weapons", code = 264_1109, classification = ItemClassification.useful, ),
|
||||
"Grand Mallet": KH1ItemData("Weapons", code = 264_1110, classification = ItemClassification.useful, ),
|
||||
"Lord Fortune": KH1ItemData("Weapons", code = 264_1111, classification = ItemClassification.useful, ),
|
||||
"Violetta": KH1ItemData("Weapons", code = 264_1112, classification = ItemClassification.useful, ),
|
||||
"Dream Rod (Donald)": KH1ItemData("Weapons", code = 264_1113, classification = ItemClassification.useful, ),
|
||||
"Save the Queen": KH1ItemData("Weapons", code = 264_1114, classification = ItemClassification.useful, ),
|
||||
"Wizard's Relic": KH1ItemData("Weapons", code = 264_1115, classification = ItemClassification.useful, ),
|
||||
"Meteor Strike": KH1ItemData("Weapons", code = 264_1116, classification = ItemClassification.useful, ),
|
||||
"Fantasista": KH1ItemData("Weapons", code = 264_1117, classification = ItemClassification.useful, ),
|
||||
#"Unused (Donald)": KH1ItemData("Weapons", code = 264_1118, classification = ItemClassification.filler, ),
|
||||
#"Knight's Shield": KH1ItemData("Weapons", code = 264_1119, classification = ItemClassification.filler, ),
|
||||
"Mythril Shield": KH1ItemData("Weapons", code = 264_1120, classification = ItemClassification.useful, ),
|
||||
"Onyx Shield": KH1ItemData("Weapons", code = 264_1121, classification = ItemClassification.useful, ),
|
||||
"Stout Shield": KH1ItemData("Weapons", code = 264_1122, classification = ItemClassification.useful, ),
|
||||
"Golem Shield": KH1ItemData("Weapons", code = 264_1123, classification = ItemClassification.useful, ),
|
||||
"Adamant Shield": KH1ItemData("Weapons", code = 264_1124, classification = ItemClassification.useful, ),
|
||||
"Smasher": KH1ItemData("Weapons", code = 264_1125, classification = ItemClassification.useful, ),
|
||||
"Gigas Fist": KH1ItemData("Weapons", code = 264_1126, classification = ItemClassification.useful, ),
|
||||
"Genji Shield": KH1ItemData("Weapons", code = 264_1127, classification = ItemClassification.useful, ),
|
||||
"Herc's Shield": KH1ItemData("Weapons", code = 264_1128, classification = ItemClassification.useful, ),
|
||||
"Dream Shield (Goofy)": KH1ItemData("Weapons", code = 264_1129, classification = ItemClassification.useful, ),
|
||||
"Save the King": KH1ItemData("Weapons", code = 264_1130, classification = ItemClassification.useful, ),
|
||||
"Defender": KH1ItemData("Weapons", code = 264_1131, classification = ItemClassification.useful, ),
|
||||
"Mighty Shield": KH1ItemData("Weapons", code = 264_1132, classification = ItemClassification.useful, ),
|
||||
"Seven Elements": KH1ItemData("Weapons", code = 264_1133, classification = ItemClassification.useful, ),
|
||||
#"Unused (Goofy)": KH1ItemData("Weapons", code = 264_1134, classification = ItemClassification.filler, ),
|
||||
#"Spear": KH1ItemData("Weapons", code = 264_1135, classification = ItemClassification.filler, ),
|
||||
#"No Weapon": KH1ItemData("Weapons", code = 264_1136, classification = ItemClassification.filler, ),
|
||||
#"Genie": KH1ItemData("Weapons", code = 264_1137, classification = ItemClassification.filler, ),
|
||||
#"No Weapon": KH1ItemData("Weapons", code = 264_1138, classification = ItemClassification.filler, ),
|
||||
#"No Weapon": KH1ItemData("Weapons", code = 264_1139, classification = ItemClassification.filler, ),
|
||||
#"Tinker Bell": KH1ItemData("Weapons", code = 264_1140, classification = ItemClassification.filler, ),
|
||||
#"Claws": KH1ItemData("Weapons", code = 264_1141, classification = ItemClassification.filler, ),
|
||||
"Tent": KH1ItemData("Camping", code = 264_1142, classification = ItemClassification.filler, ),
|
||||
"Camping Set": KH1ItemData("Camping", code = 264_1143, classification = ItemClassification.filler, ),
|
||||
"Cottage": KH1ItemData("Camping", code = 264_1144, classification = ItemClassification.filler, ),
|
||||
#"C04": KH1ItemData("Camping", code = 264_1145, classification = ItemClassification.filler, ),
|
||||
#"C05": KH1ItemData("Camping", code = 264_1146, classification = ItemClassification.filler, ),
|
||||
#"C06": KH1ItemData("Camping", code = 264_1147, classification = ItemClassification.filler, ),
|
||||
#"C07": KH1ItemData("Camping", code = 264_1148, classification = ItemClassification.filler, ),
|
||||
"Ansem's Report 11": KH1ItemData("Reports", code = 264_1149, classification = ItemClassification.progression, ),
|
||||
"Ansem's Report 12": KH1ItemData("Reports", code = 264_1150, classification = ItemClassification.progression, ),
|
||||
"Ansem's Report 13": KH1ItemData("Reports", code = 264_1151, classification = ItemClassification.progression, ),
|
||||
"Power Up": KH1ItemData("Stat Ups", code = 264_1152, classification = ItemClassification.filler, ),
|
||||
"Defense Up": KH1ItemData("Stat Ups", code = 264_1153, classification = ItemClassification.filler, ),
|
||||
"AP Up": KH1ItemData("Stat Ups", code = 264_1154, classification = ItemClassification.filler, ),
|
||||
#"Serenity Power": KH1ItemData("Synthesis", code = 264_1155, classification = ItemClassification.filler, ),
|
||||
#"Dark Matter": KH1ItemData("Synthesis", code = 264_1156, classification = ItemClassification.filler, ),
|
||||
#"Mythril Stone": KH1ItemData("Synthesis", code = 264_1157, classification = ItemClassification.filler, ),
|
||||
"Fire Arts": KH1ItemData("Key", code = 264_1158, classification = ItemClassification.progression, ),
|
||||
"Blizzard Arts": KH1ItemData("Key", code = 264_1159, classification = ItemClassification.progression, ),
|
||||
"Thunder Arts": KH1ItemData("Key", code = 264_1160, classification = ItemClassification.progression, ),
|
||||
"Cure Arts": KH1ItemData("Key", code = 264_1161, classification = ItemClassification.progression, ),
|
||||
"Gravity Arts": KH1ItemData("Key", code = 264_1162, classification = ItemClassification.progression, ),
|
||||
"Stop Arts": KH1ItemData("Key", code = 264_1163, classification = ItemClassification.progression, ),
|
||||
"Aero Arts": KH1ItemData("Key", code = 264_1164, classification = ItemClassification.progression, ),
|
||||
#"Shiitank Rank": KH1ItemData("Synthesis", code = 264_1165, classification = ItemClassification.filler, ),
|
||||
#"Matsutake Rank": KH1ItemData("Synthesis", code = 264_1166, classification = ItemClassification.filler, ),
|
||||
#"Mystery Mold": KH1ItemData("Synthesis", code = 264_1167, classification = ItemClassification.filler, ),
|
||||
"Ansem's Report 1": KH1ItemData("Reports", code = 264_1168, classification = ItemClassification.progression, ),
|
||||
"Ansem's Report 2": KH1ItemData("Reports", code = 264_1169, classification = ItemClassification.progression, ),
|
||||
"Ansem's Report 3": KH1ItemData("Reports", code = 264_1170, classification = ItemClassification.progression, ),
|
||||
"Ansem's Report 4": KH1ItemData("Reports", code = 264_1171, classification = ItemClassification.progression, ),
|
||||
"Ansem's Report 5": KH1ItemData("Reports", code = 264_1172, classification = ItemClassification.progression, ),
|
||||
"Ansem's Report 6": KH1ItemData("Reports", code = 264_1173, classification = ItemClassification.progression, ),
|
||||
"Ansem's Report 7": KH1ItemData("Reports", code = 264_1174, classification = ItemClassification.progression, ),
|
||||
"Ansem's Report 8": KH1ItemData("Reports", code = 264_1175, classification = ItemClassification.progression, ),
|
||||
"Ansem's Report 9": KH1ItemData("Reports", code = 264_1176, classification = ItemClassification.progression, ),
|
||||
"Ansem's Report 10": KH1ItemData("Reports", code = 264_1177, classification = ItemClassification.progression, ),
|
||||
#"Khama Vol. 8": KH1ItemData("Key", code = 264_1178, classification = ItemClassification.progression, ),
|
||||
#"Salegg Vol. 6": KH1ItemData("Key", code = 264_1179, classification = ItemClassification.progression, ),
|
||||
#"Azal Vol. 3": KH1ItemData("Key", code = 264_1180, classification = ItemClassification.progression, ),
|
||||
#"Mava Vol. 3": KH1ItemData("Key", code = 264_1181, classification = ItemClassification.progression, ),
|
||||
#"Mava Vol. 6": KH1ItemData("Key", code = 264_1182, classification = ItemClassification.progression, ),
|
||||
"Theon Vol. 6": KH1ItemData("Key", code = 264_1183, classification = ItemClassification.progression, ),
|
||||
#"Nahara Vol. 5": KH1ItemData("Key", code = 264_1184, classification = ItemClassification.progression, ),
|
||||
#"Hafet Vol. 4": KH1ItemData("Key", code = 264_1185, classification = ItemClassification.progression, ),
|
||||
"Empty Bottle": KH1ItemData("Key", code = 264_1186, classification = ItemClassification.progression, max_quantity = 6 ),
|
||||
#"Old Book": KH1ItemData("Key", code = 264_1187, classification = ItemClassification.progression, ),
|
||||
"Emblem Piece (Flame)": KH1ItemData("Key", code = 264_1188, classification = ItemClassification.progression, ),
|
||||
"Emblem Piece (Chest)": KH1ItemData("Key", code = 264_1189, classification = ItemClassification.progression, ),
|
||||
"Emblem Piece (Statue)": KH1ItemData("Key", code = 264_1190, classification = ItemClassification.progression, ),
|
||||
"Emblem Piece (Fountain)": KH1ItemData("Key", code = 264_1191, classification = ItemClassification.progression, ),
|
||||
#"Log": KH1ItemData("Key", code = 264_1192, classification = ItemClassification.progression, ),
|
||||
#"Cloth": KH1ItemData("Key", code = 264_1193, classification = ItemClassification.progression, ),
|
||||
#"Rope": KH1ItemData("Key", code = 264_1194, classification = ItemClassification.progression, ),
|
||||
#"Seagull Egg": KH1ItemData("Key", code = 264_1195, classification = ItemClassification.progression, ),
|
||||
#"Fish": KH1ItemData("Key", code = 264_1196, classification = ItemClassification.progression, ),
|
||||
#"Mushroom": KH1ItemData("Key", code = 264_1197, classification = ItemClassification.progression, ),
|
||||
#"Coconut": KH1ItemData("Key", code = 264_1198, classification = ItemClassification.progression, ),
|
||||
#"Drinking Water": KH1ItemData("Key", code = 264_1199, classification = ItemClassification.progression, ),
|
||||
#"Navi-G Piece 1": KH1ItemData("Key", code = 264_1200, classification = ItemClassification.progression, ),
|
||||
#"Navi-G Piece 2": KH1ItemData("Key", code = 264_1201, classification = ItemClassification.progression, ),
|
||||
#"Navi-Gummi Unused": KH1ItemData("Key", code = 264_1202, classification = ItemClassification.progression, ),
|
||||
#"Navi-G Piece 3": KH1ItemData("Key", code = 264_1203, classification = ItemClassification.progression, ),
|
||||
#"Navi-G Piece 4": KH1ItemData("Key", code = 264_1204, classification = ItemClassification.progression, ),
|
||||
#"Navi-Gummi": KH1ItemData("Key", code = 264_1205, classification = ItemClassification.progression, ),
|
||||
#"Watergleam": KH1ItemData("Key", code = 264_1206, classification = ItemClassification.progression, ),
|
||||
#"Naturespark": KH1ItemData("Key", code = 264_1207, classification = ItemClassification.progression, ),
|
||||
#"Fireglow": KH1ItemData("Key", code = 264_1208, classification = ItemClassification.progression, ),
|
||||
#"Earthshine": KH1ItemData("Key", code = 264_1209, classification = ItemClassification.progression, ),
|
||||
"Crystal Trident": KH1ItemData("Key", code = 264_1210, classification = ItemClassification.progression, ),
|
||||
"Postcard": KH1ItemData("Key", code = 264_1211, classification = ItemClassification.progression, max_quantity = 10),
|
||||
"Torn Page 1": KH1ItemData("Torn Pages", code = 264_1212, classification = ItemClassification.progression, ),
|
||||
"Torn Page 2": KH1ItemData("Torn Pages", code = 264_1213, classification = ItemClassification.progression, ),
|
||||
"Torn Page 3": KH1ItemData("Torn Pages", code = 264_1214, classification = ItemClassification.progression, ),
|
||||
"Torn Page 4": KH1ItemData("Torn Pages", code = 264_1215, classification = ItemClassification.progression, ),
|
||||
"Torn Page 5": KH1ItemData("Torn Pages", code = 264_1216, classification = ItemClassification.progression, ),
|
||||
"Slides": KH1ItemData("Key", code = 264_1217, classification = ItemClassification.progression, ),
|
||||
#"Slide 2": KH1ItemData("Key", code = 264_1218, classification = ItemClassification.progression, ),
|
||||
#"Slide 3": KH1ItemData("Key", code = 264_1219, classification = ItemClassification.progression, ),
|
||||
#"Slide 4": KH1ItemData("Key", code = 264_1220, classification = ItemClassification.progression, ),
|
||||
#"Slide 5": KH1ItemData("Key", code = 264_1221, classification = ItemClassification.progression, ),
|
||||
#"Slide 6": KH1ItemData("Key", code = 264_1222, classification = ItemClassification.progression, ),
|
||||
"Footprints": KH1ItemData("Key", code = 264_1223, classification = ItemClassification.progression, ),
|
||||
#"Claw Marks": KH1ItemData("Key", code = 264_1224, classification = ItemClassification.progression, ),
|
||||
#"Stench": KH1ItemData("Key", code = 264_1225, classification = ItemClassification.progression, ),
|
||||
#"Antenna": KH1ItemData("Key", code = 264_1226, classification = ItemClassification.progression, ),
|
||||
"Forget-Me-Not": KH1ItemData("Key", code = 264_1227, classification = ItemClassification.progression, ),
|
||||
"Jack-In-The-Box": KH1ItemData("Key", code = 264_1228, classification = ItemClassification.progression, ),
|
||||
"Entry Pass": KH1ItemData("Key", code = 264_1229, classification = ItemClassification.progression, ),
|
||||
#"Hero License": KH1ItemData("Key", code = 264_1230, classification = ItemClassification.progression, ),
|
||||
#"Pretty Stone": KH1ItemData("Synthesis", code = 264_1231, classification = ItemClassification.filler, ),
|
||||
#"N41": KH1ItemData("Synthesis", code = 264_1232, classification = ItemClassification.filler, ),
|
||||
#"Lucid Shard": KH1ItemData("Synthesis", code = 264_1233, classification = ItemClassification.filler, ),
|
||||
#"Lucid Gem": KH1ItemData("Synthesis", code = 264_1234, classification = ItemClassification.filler, ),
|
||||
#"Lucid Crystal": KH1ItemData("Synthesis", code = 264_1235, classification = ItemClassification.filler, ),
|
||||
#"Spirit Shard": KH1ItemData("Synthesis", code = 264_1236, classification = ItemClassification.filler, ),
|
||||
#"Spirit Gem": KH1ItemData("Synthesis", code = 264_1237, classification = ItemClassification.filler, ),
|
||||
#"Power Shard": KH1ItemData("Synthesis", code = 264_1238, classification = ItemClassification.filler, ),
|
||||
#"Power Gem": KH1ItemData("Synthesis", code = 264_1239, classification = ItemClassification.filler, ),
|
||||
#"Power Crystal": KH1ItemData("Synthesis", code = 264_1240, classification = ItemClassification.filler, ),
|
||||
#"Blaze Shard": KH1ItemData("Synthesis", code = 264_1241, classification = ItemClassification.filler, ),
|
||||
#"Blaze Gem": KH1ItemData("Synthesis", code = 264_1242, classification = ItemClassification.filler, ),
|
||||
#"Frost Shard": KH1ItemData("Synthesis", code = 264_1243, classification = ItemClassification.filler, ),
|
||||
#"Frost Gem": KH1ItemData("Synthesis", code = 264_1244, classification = ItemClassification.filler, ),
|
||||
#"Thunder Shard": KH1ItemData("Synthesis", code = 264_1245, classification = ItemClassification.filler, ),
|
||||
#"Thunder Gem": KH1ItemData("Synthesis", code = 264_1246, classification = ItemClassification.filler, ),
|
||||
#"Shiny Crystal": KH1ItemData("Synthesis", code = 264_1247, classification = ItemClassification.filler, ),
|
||||
#"Bright Shard": KH1ItemData("Synthesis", code = 264_1248, classification = ItemClassification.filler, ),
|
||||
#"Bright Gem": KH1ItemData("Synthesis", code = 264_1249, classification = ItemClassification.filler, ),
|
||||
#"Bright Crystal": KH1ItemData("Synthesis", code = 264_1250, classification = ItemClassification.filler, ),
|
||||
#"Mystery Goo": KH1ItemData("Synthesis", code = 264_1251, classification = ItemClassification.filler, ),
|
||||
#"Gale": KH1ItemData("Synthesis", code = 264_1252, classification = ItemClassification.filler, ),
|
||||
#"Mythril Shard": KH1ItemData("Synthesis", code = 264_1253, classification = ItemClassification.filler, ),
|
||||
#"Mythril": KH1ItemData("Synthesis", code = 264_1254, classification = ItemClassification.filler, ),
|
||||
#"Orichalcum": KH1ItemData("Synthesis", code = 264_1255, classification = ItemClassification.filler, ),
|
||||
"High Jump": KH1ItemData("Shared Abilities", code = 264_2001, classification = ItemClassification.progression, ),
|
||||
"Mermaid Kick": KH1ItemData("Shared Abilities", code = 264_2002, classification = ItemClassification.progression, ),
|
||||
"Progressive Glide": KH1ItemData("Shared Abilities", code = 264_2003, classification = ItemClassification.progression, max_quantity = 2 ),
|
||||
#"Superglide": KH1ItemData("Shared Abilities", code = 264_2004, classification = ItemClassification.progression, ),
|
||||
"Puppy 01": KH1ItemData("Puppies", code = 264_2101, classification = ItemClassification.progression, ),
|
||||
"Puppy 02": KH1ItemData("Puppies", code = 264_2102, classification = ItemClassification.progression, ),
|
||||
"Puppy 03": KH1ItemData("Puppies", code = 264_2103, classification = ItemClassification.progression, ),
|
||||
"Puppy 04": KH1ItemData("Puppies", code = 264_2104, classification = ItemClassification.progression, ),
|
||||
"Puppy 05": KH1ItemData("Puppies", code = 264_2105, classification = ItemClassification.progression, ),
|
||||
"Puppy 06": KH1ItemData("Puppies", code = 264_2106, classification = ItemClassification.progression, ),
|
||||
"Puppy 07": KH1ItemData("Puppies", code = 264_2107, classification = ItemClassification.progression, ),
|
||||
"Puppy 08": KH1ItemData("Puppies", code = 264_2108, classification = ItemClassification.progression, ),
|
||||
"Puppy 09": KH1ItemData("Puppies", code = 264_2109, classification = ItemClassification.progression, ),
|
||||
"Puppy 10": KH1ItemData("Puppies", code = 264_2110, classification = ItemClassification.progression, ),
|
||||
"Puppy 11": KH1ItemData("Puppies", code = 264_2111, classification = ItemClassification.progression, ),
|
||||
"Puppy 12": KH1ItemData("Puppies", code = 264_2112, classification = ItemClassification.progression, ),
|
||||
"Puppy 13": KH1ItemData("Puppies", code = 264_2113, classification = ItemClassification.progression, ),
|
||||
"Puppy 14": KH1ItemData("Puppies", code = 264_2114, classification = ItemClassification.progression, ),
|
||||
"Puppy 15": KH1ItemData("Puppies", code = 264_2115, classification = ItemClassification.progression, ),
|
||||
"Puppy 16": KH1ItemData("Puppies", code = 264_2116, classification = ItemClassification.progression, ),
|
||||
"Puppy 17": KH1ItemData("Puppies", code = 264_2117, classification = ItemClassification.progression, ),
|
||||
"Puppy 18": KH1ItemData("Puppies", code = 264_2118, classification = ItemClassification.progression, ),
|
||||
"Puppy 19": KH1ItemData("Puppies", code = 264_2119, classification = ItemClassification.progression, ),
|
||||
"Puppy 20": KH1ItemData("Puppies", code = 264_2120, classification = ItemClassification.progression, ),
|
||||
"Puppy 21": KH1ItemData("Puppies", code = 264_2121, classification = ItemClassification.progression, ),
|
||||
"Puppy 22": KH1ItemData("Puppies", code = 264_2122, classification = ItemClassification.progression, ),
|
||||
"Puppy 23": KH1ItemData("Puppies", code = 264_2123, classification = ItemClassification.progression, ),
|
||||
"Puppy 24": KH1ItemData("Puppies", code = 264_2124, classification = ItemClassification.progression, ),
|
||||
"Puppy 25": KH1ItemData("Puppies", code = 264_2125, classification = ItemClassification.progression, ),
|
||||
"Puppy 26": KH1ItemData("Puppies", code = 264_2126, classification = ItemClassification.progression, ),
|
||||
"Puppy 27": KH1ItemData("Puppies", code = 264_2127, classification = ItemClassification.progression, ),
|
||||
"Puppy 28": KH1ItemData("Puppies", code = 264_2128, classification = ItemClassification.progression, ),
|
||||
"Puppy 29": KH1ItemData("Puppies", code = 264_2129, classification = ItemClassification.progression, ),
|
||||
"Puppy 30": KH1ItemData("Puppies", code = 264_2130, classification = ItemClassification.progression, ),
|
||||
"Puppy 31": KH1ItemData("Puppies", code = 264_2131, classification = ItemClassification.progression, ),
|
||||
"Puppy 32": KH1ItemData("Puppies", code = 264_2132, classification = ItemClassification.progression, ),
|
||||
"Puppy 33": KH1ItemData("Puppies", code = 264_2133, classification = ItemClassification.progression, ),
|
||||
"Puppy 34": KH1ItemData("Puppies", code = 264_2134, classification = ItemClassification.progression, ),
|
||||
"Puppy 35": KH1ItemData("Puppies", code = 264_2135, classification = ItemClassification.progression, ),
|
||||
"Puppy 36": KH1ItemData("Puppies", code = 264_2136, classification = ItemClassification.progression, ),
|
||||
"Puppy 37": KH1ItemData("Puppies", code = 264_2137, classification = ItemClassification.progression, ),
|
||||
"Puppy 38": KH1ItemData("Puppies", code = 264_2138, classification = ItemClassification.progression, ),
|
||||
"Puppy 39": KH1ItemData("Puppies", code = 264_2139, classification = ItemClassification.progression, ),
|
||||
"Puppy 40": KH1ItemData("Puppies", code = 264_2140, classification = ItemClassification.progression, ),
|
||||
"Puppy 41": KH1ItemData("Puppies", code = 264_2141, classification = ItemClassification.progression, ),
|
||||
"Puppy 42": KH1ItemData("Puppies", code = 264_2142, classification = ItemClassification.progression, ),
|
||||
"Puppy 43": KH1ItemData("Puppies", code = 264_2143, classification = ItemClassification.progression, ),
|
||||
"Puppy 44": KH1ItemData("Puppies", code = 264_2144, classification = ItemClassification.progression, ),
|
||||
"Puppy 45": KH1ItemData("Puppies", code = 264_2145, classification = ItemClassification.progression, ),
|
||||
"Puppy 46": KH1ItemData("Puppies", code = 264_2146, classification = ItemClassification.progression, ),
|
||||
"Puppy 47": KH1ItemData("Puppies", code = 264_2147, classification = ItemClassification.progression, ),
|
||||
"Puppy 48": KH1ItemData("Puppies", code = 264_2148, classification = ItemClassification.progression, ),
|
||||
"Puppy 49": KH1ItemData("Puppies", code = 264_2149, classification = ItemClassification.progression, ),
|
||||
"Puppy 50": KH1ItemData("Puppies", code = 264_2150, classification = ItemClassification.progression, ),
|
||||
"Puppy 51": KH1ItemData("Puppies", code = 264_2151, classification = ItemClassification.progression, ),
|
||||
"Puppy 52": KH1ItemData("Puppies", code = 264_2152, classification = ItemClassification.progression, ),
|
||||
"Puppy 53": KH1ItemData("Puppies", code = 264_2153, classification = ItemClassification.progression, ),
|
||||
"Puppy 54": KH1ItemData("Puppies", code = 264_2154, classification = ItemClassification.progression, ),
|
||||
"Puppy 55": KH1ItemData("Puppies", code = 264_2155, classification = ItemClassification.progression, ),
|
||||
"Puppy 56": KH1ItemData("Puppies", code = 264_2156, classification = ItemClassification.progression, ),
|
||||
"Puppy 57": KH1ItemData("Puppies", code = 264_2157, classification = ItemClassification.progression, ),
|
||||
"Puppy 58": KH1ItemData("Puppies", code = 264_2158, classification = ItemClassification.progression, ),
|
||||
"Puppy 59": KH1ItemData("Puppies", code = 264_2159, classification = ItemClassification.progression, ),
|
||||
"Puppy 60": KH1ItemData("Puppies", code = 264_2160, classification = ItemClassification.progression, ),
|
||||
"Puppy 61": KH1ItemData("Puppies", code = 264_2161, classification = ItemClassification.progression, ),
|
||||
"Puppy 62": KH1ItemData("Puppies", code = 264_2162, classification = ItemClassification.progression, ),
|
||||
"Puppy 63": KH1ItemData("Puppies", code = 264_2163, classification = ItemClassification.progression, ),
|
||||
"Puppy 64": KH1ItemData("Puppies", code = 264_2164, classification = ItemClassification.progression, ),
|
||||
"Puppy 65": KH1ItemData("Puppies", code = 264_2165, classification = ItemClassification.progression, ),
|
||||
"Puppy 66": KH1ItemData("Puppies", code = 264_2166, classification = ItemClassification.progression, ),
|
||||
"Puppy 67": KH1ItemData("Puppies", code = 264_2167, classification = ItemClassification.progression, ),
|
||||
"Puppy 68": KH1ItemData("Puppies", code = 264_2168, classification = ItemClassification.progression, ),
|
||||
"Puppy 69": KH1ItemData("Puppies", code = 264_2169, classification = ItemClassification.progression, ),
|
||||
"Puppy 70": KH1ItemData("Puppies", code = 264_2170, classification = ItemClassification.progression, ),
|
||||
"Puppy 71": KH1ItemData("Puppies", code = 264_2171, classification = ItemClassification.progression, ),
|
||||
"Puppy 72": KH1ItemData("Puppies", code = 264_2172, classification = ItemClassification.progression, ),
|
||||
"Puppy 73": KH1ItemData("Puppies", code = 264_2173, classification = ItemClassification.progression, ),
|
||||
"Puppy 74": KH1ItemData("Puppies", code = 264_2174, classification = ItemClassification.progression, ),
|
||||
"Puppy 75": KH1ItemData("Puppies", code = 264_2175, classification = ItemClassification.progression, ),
|
||||
"Puppy 76": KH1ItemData("Puppies", code = 264_2176, classification = ItemClassification.progression, ),
|
||||
"Puppy 77": KH1ItemData("Puppies", code = 264_2177, classification = ItemClassification.progression, ),
|
||||
"Puppy 78": KH1ItemData("Puppies", code = 264_2178, classification = ItemClassification.progression, ),
|
||||
"Puppy 79": KH1ItemData("Puppies", code = 264_2179, classification = ItemClassification.progression, ),
|
||||
"Puppy 80": KH1ItemData("Puppies", code = 264_2180, classification = ItemClassification.progression, ),
|
||||
"Puppy 81": KH1ItemData("Puppies", code = 264_2181, classification = ItemClassification.progression, ),
|
||||
"Puppy 82": KH1ItemData("Puppies", code = 264_2182, classification = ItemClassification.progression, ),
|
||||
"Puppy 83": KH1ItemData("Puppies", code = 264_2183, classification = ItemClassification.progression, ),
|
||||
"Puppy 84": KH1ItemData("Puppies", code = 264_2184, classification = ItemClassification.progression, ),
|
||||
"Puppy 85": KH1ItemData("Puppies", code = 264_2185, classification = ItemClassification.progression, ),
|
||||
"Puppy 86": KH1ItemData("Puppies", code = 264_2186, classification = ItemClassification.progression, ),
|
||||
"Puppy 87": KH1ItemData("Puppies", code = 264_2187, classification = ItemClassification.progression, ),
|
||||
"Puppy 88": KH1ItemData("Puppies", code = 264_2188, classification = ItemClassification.progression, ),
|
||||
"Puppy 89": KH1ItemData("Puppies", code = 264_2189, classification = ItemClassification.progression, ),
|
||||
"Puppy 90": KH1ItemData("Puppies", code = 264_2190, classification = ItemClassification.progression, ),
|
||||
"Puppy 91": KH1ItemData("Puppies", code = 264_2191, classification = ItemClassification.progression, ),
|
||||
"Puppy 92": KH1ItemData("Puppies", code = 264_2192, classification = ItemClassification.progression, ),
|
||||
"Puppy 93": KH1ItemData("Puppies", code = 264_2193, classification = ItemClassification.progression, ),
|
||||
"Puppy 94": KH1ItemData("Puppies", code = 264_2194, classification = ItemClassification.progression, ),
|
||||
"Puppy 95": KH1ItemData("Puppies", code = 264_2195, classification = ItemClassification.progression, ),
|
||||
"Puppy 96": KH1ItemData("Puppies", code = 264_2196, classification = ItemClassification.progression, ),
|
||||
"Puppy 97": KH1ItemData("Puppies", code = 264_2197, classification = ItemClassification.progression, ),
|
||||
"Puppy 98": KH1ItemData("Puppies", code = 264_2198, classification = ItemClassification.progression, ),
|
||||
"Puppy 99": KH1ItemData("Puppies", code = 264_2199, classification = ItemClassification.progression, ),
|
||||
"Puppies 01-03": KH1ItemData("Puppies", code = 264_2201, classification = ItemClassification.progression, ),
|
||||
"Puppies 04-06": KH1ItemData("Puppies", code = 264_2202, classification = ItemClassification.progression, ),
|
||||
"Puppies 07-09": KH1ItemData("Puppies", code = 264_2203, classification = ItemClassification.progression, ),
|
||||
"Puppies 10-12": KH1ItemData("Puppies", code = 264_2204, classification = ItemClassification.progression, ),
|
||||
"Puppies 13-15": KH1ItemData("Puppies", code = 264_2205, classification = ItemClassification.progression, ),
|
||||
"Puppies 16-18": KH1ItemData("Puppies", code = 264_2206, classification = ItemClassification.progression, ),
|
||||
"Puppies 19-21": KH1ItemData("Puppies", code = 264_2207, classification = ItemClassification.progression, ),
|
||||
"Puppies 22-24": KH1ItemData("Puppies", code = 264_2208, classification = ItemClassification.progression, ),
|
||||
"Puppies 25-27": KH1ItemData("Puppies", code = 264_2209, classification = ItemClassification.progression, ),
|
||||
"Puppies 28-30": KH1ItemData("Puppies", code = 264_2210, classification = ItemClassification.progression, ),
|
||||
"Puppies 31-33": KH1ItemData("Puppies", code = 264_2211, classification = ItemClassification.progression, ),
|
||||
"Puppies 34-36": KH1ItemData("Puppies", code = 264_2212, classification = ItemClassification.progression, ),
|
||||
"Puppies 37-39": KH1ItemData("Puppies", code = 264_2213, classification = ItemClassification.progression, ),
|
||||
"Puppies 40-42": KH1ItemData("Puppies", code = 264_2214, classification = ItemClassification.progression, ),
|
||||
"Puppies 43-45": KH1ItemData("Puppies", code = 264_2215, classification = ItemClassification.progression, ),
|
||||
"Puppies 46-48": KH1ItemData("Puppies", code = 264_2216, classification = ItemClassification.progression, ),
|
||||
"Puppies 49-51": KH1ItemData("Puppies", code = 264_2217, classification = ItemClassification.progression, ),
|
||||
"Puppies 52-54": KH1ItemData("Puppies", code = 264_2218, classification = ItemClassification.progression, ),
|
||||
"Puppies 55-57": KH1ItemData("Puppies", code = 264_2219, classification = ItemClassification.progression, ),
|
||||
"Puppies 58-60": KH1ItemData("Puppies", code = 264_2220, classification = ItemClassification.progression, ),
|
||||
"Puppies 61-63": KH1ItemData("Puppies", code = 264_2221, classification = ItemClassification.progression, ),
|
||||
"Puppies 64-66": KH1ItemData("Puppies", code = 264_2222, classification = ItemClassification.progression, ),
|
||||
"Puppies 67-69": KH1ItemData("Puppies", code = 264_2223, classification = ItemClassification.progression, ),
|
||||
"Puppies 70-72": KH1ItemData("Puppies", code = 264_2224, classification = ItemClassification.progression, ),
|
||||
"Puppies 73-75": KH1ItemData("Puppies", code = 264_2225, classification = ItemClassification.progression, ),
|
||||
"Puppies 76-78": KH1ItemData("Puppies", code = 264_2226, classification = ItemClassification.progression, ),
|
||||
"Puppies 79-81": KH1ItemData("Puppies", code = 264_2227, classification = ItemClassification.progression, ),
|
||||
"Puppies 82-84": KH1ItemData("Puppies", code = 264_2228, classification = ItemClassification.progression, ),
|
||||
"Puppies 85-87": KH1ItemData("Puppies", code = 264_2229, classification = ItemClassification.progression, ),
|
||||
"Puppies 88-90": KH1ItemData("Puppies", code = 264_2230, classification = ItemClassification.progression, ),
|
||||
"Puppies 91-93": KH1ItemData("Puppies", code = 264_2231, classification = ItemClassification.progression, ),
|
||||
"Puppies 94-96": KH1ItemData("Puppies", code = 264_2232, classification = ItemClassification.progression, ),
|
||||
"Puppies 97-99": KH1ItemData("Puppies", code = 264_2233, classification = ItemClassification.progression, ),
|
||||
"All Puppies": KH1ItemData("Puppies", code = 264_2240, classification = ItemClassification.progression, ),
|
||||
"Treasure Magnet": KH1ItemData("Abilities", code = 264_3005, classification = ItemClassification.useful, max_quantity = 2 ),
|
||||
"Combo Plus": KH1ItemData("Abilities", code = 264_3006, classification = ItemClassification.useful, max_quantity = 4 ),
|
||||
"Air Combo Plus": KH1ItemData("Abilities", code = 264_3007, classification = ItemClassification.useful, max_quantity = 2 ),
|
||||
"Critical Plus": KH1ItemData("Abilities", code = 264_3008, classification = ItemClassification.useful, max_quantity = 3 ),
|
||||
#"Second Wind": KH1ItemData("Abilities", code = 264_3009, classification = ItemClassification.useful, ),
|
||||
"Scan": KH1ItemData("Abilities", code = 264_3010, classification = ItemClassification.useful, ),
|
||||
"Sonic Blade": KH1ItemData("Abilities", code = 264_3011, classification = ItemClassification.useful, ),
|
||||
"Ars Arcanum": KH1ItemData("Abilities", code = 264_3012, classification = ItemClassification.useful, ),
|
||||
"Strike Raid": KH1ItemData("Abilities", code = 264_3013, classification = ItemClassification.useful, ),
|
||||
"Ragnarok": KH1ItemData("Abilities", code = 264_3014, classification = ItemClassification.useful, ),
|
||||
"Trinity Limit": KH1ItemData("Abilities", code = 264_3015, classification = ItemClassification.useful, ),
|
||||
"Cheer": KH1ItemData("Abilities", code = 264_3016, classification = ItemClassification.useful, ),
|
||||
"Vortex": KH1ItemData("Abilities", code = 264_3017, classification = ItemClassification.useful, ),
|
||||
"Aerial Sweep": KH1ItemData("Abilities", code = 264_3018, classification = ItemClassification.useful, ),
|
||||
"Counterattack": KH1ItemData("Abilities", code = 264_3019, classification = ItemClassification.useful, ),
|
||||
"Blitz": KH1ItemData("Abilities", code = 264_3020, classification = ItemClassification.useful, ),
|
||||
"Guard": KH1ItemData("Abilities", code = 264_3021, classification = ItemClassification.progression, ),
|
||||
"Dodge Roll": KH1ItemData("Abilities", code = 264_3022, classification = ItemClassification.progression, ),
|
||||
"MP Haste": KH1ItemData("Abilities", code = 264_3023, classification = ItemClassification.useful, ),
|
||||
"MP Rage": KH1ItemData("Abilities", code = 264_3024, classification = ItemClassification.progression, ),
|
||||
"Second Chance": KH1ItemData("Abilities", code = 264_3025, classification = ItemClassification.progression, ),
|
||||
"Berserk": KH1ItemData("Abilities", code = 264_3026, classification = ItemClassification.useful, ),
|
||||
"Jackpot": KH1ItemData("Abilities", code = 264_3027, classification = ItemClassification.useful, ),
|
||||
"Lucky Strike": KH1ItemData("Abilities", code = 264_3028, classification = ItemClassification.useful, ),
|
||||
#"Charge": KH1ItemData("Abilities", code = 264_3029, classification = ItemClassification.useful, ),
|
||||
#"Rocket": KH1ItemData("Abilities", code = 264_3030, classification = ItemClassification.useful, ),
|
||||
#"Tornado": KH1ItemData("Abilities", code = 264_3031, classification = ItemClassification.useful, ),
|
||||
#"MP Gift": KH1ItemData("Abilities", code = 264_3032, classification = ItemClassification.useful, ),
|
||||
#"Raging Boar": KH1ItemData("Abilities", code = 264_3033, classification = ItemClassification.useful, ),
|
||||
#"Asp's Bite": KH1ItemData("Abilities", code = 264_3034, classification = ItemClassification.useful, ),
|
||||
#"Healing Herb": KH1ItemData("Abilities", code = 264_3035, classification = ItemClassification.useful, ),
|
||||
#"Wind Armor": KH1ItemData("Abilities", code = 264_3036, classification = ItemClassification.useful, ),
|
||||
#"Crescent": KH1ItemData("Abilities", code = 264_3037, classification = ItemClassification.useful, ),
|
||||
#"Sandstorm": KH1ItemData("Abilities", code = 264_3038, classification = ItemClassification.useful, ),
|
||||
#"Applause!": KH1ItemData("Abilities", code = 264_3039, classification = ItemClassification.useful, ),
|
||||
#"Blazing Fury": KH1ItemData("Abilities", code = 264_3040, classification = ItemClassification.useful, ),
|
||||
#"Icy Terror": KH1ItemData("Abilities", code = 264_3041, classification = ItemClassification.useful, ),
|
||||
#"Bolts of Sorrow": KH1ItemData("Abilities", code = 264_3042, classification = ItemClassification.useful, ),
|
||||
#"Ghostly Scream": KH1ItemData("Abilities", code = 264_3043, classification = ItemClassification.useful, ),
|
||||
#"Humming Bird": KH1ItemData("Abilities", code = 264_3044, classification = ItemClassification.useful, ),
|
||||
#"Time-Out": KH1ItemData("Abilities", code = 264_3045, classification = ItemClassification.useful, ),
|
||||
#"Storm's Eye": KH1ItemData("Abilities", code = 264_3046, classification = ItemClassification.useful, ),
|
||||
#"Ferocious Lunge": KH1ItemData("Abilities", code = 264_3047, classification = ItemClassification.useful, ),
|
||||
#"Furious Bellow": KH1ItemData("Abilities", code = 264_3048, classification = ItemClassification.useful, ),
|
||||
#"Spiral Wave": KH1ItemData("Abilities", code = 264_3049, classification = ItemClassification.useful, ),
|
||||
#"Thunder Potion": KH1ItemData("Abilities", code = 264_3050, classification = ItemClassification.useful, ),
|
||||
#"Cure Potion": KH1ItemData("Abilities", code = 264_3051, classification = ItemClassification.useful, ),
|
||||
#"Aero Potion": KH1ItemData("Abilities", code = 264_3052, classification = ItemClassification.useful, ),
|
||||
"Slapshot": KH1ItemData("Abilities", code = 264_3053, classification = ItemClassification.useful, ),
|
||||
"Sliding Dash": KH1ItemData("Abilities", code = 264_3054, classification = ItemClassification.useful, ),
|
||||
"Hurricane Blast": KH1ItemData("Abilities", code = 264_3055, classification = ItemClassification.useful, ),
|
||||
"Ripple Drive": KH1ItemData("Abilities", code = 264_3056, classification = ItemClassification.useful, ),
|
||||
"Stun Impact": KH1ItemData("Abilities", code = 264_3057, classification = ItemClassification.useful, ),
|
||||
"Gravity Break": KH1ItemData("Abilities", code = 264_3058, classification = ItemClassification.useful, ),
|
||||
"Zantetsuken": KH1ItemData("Abilities", code = 264_3059, classification = ItemClassification.useful, ),
|
||||
"Tech Boost": KH1ItemData("Abilities", code = 264_3060, classification = ItemClassification.useful, max_quantity = 4 ),
|
||||
"Encounter Plus": KH1ItemData("Abilities", code = 264_3061, classification = ItemClassification.useful, ),
|
||||
"Leaf Bracer": KH1ItemData("Abilities", code = 264_3062, classification = ItemClassification.progression, ),
|
||||
#"Evolution": KH1ItemData("Abilities", code = 264_3063, classification = ItemClassification.useful, ),
|
||||
"EXP Zero": KH1ItemData("Abilities", code = 264_3064, classification = ItemClassification.useful, ),
|
||||
"Combo Master": KH1ItemData("Abilities", code = 264_3065, classification = ItemClassification.progression, ),
|
||||
"Max HP Increase": KH1ItemData("Level Up", code = 264_4001, classification = ItemClassification.useful, max_quantity = 15),
|
||||
"Max MP Increase": KH1ItemData("Level Up", code = 264_4002, classification = ItemClassification.useful, max_quantity = 15),
|
||||
"Max AP Increase": KH1ItemData("Level Up", code = 264_4003, classification = ItemClassification.useful, max_quantity = 15),
|
||||
"Strength Increase": KH1ItemData("Level Up", code = 264_4004, classification = ItemClassification.useful, max_quantity = 15),
|
||||
"Defense Increase": KH1ItemData("Level Up", code = 264_4005, classification = ItemClassification.useful, max_quantity = 15),
|
||||
"Accessory Slot Increase": KH1ItemData("Limited Level Up", code = 264_4006, classification = ItemClassification.useful, max_quantity = 15),
|
||||
"Item Slot Increase": KH1ItemData("Limited Level Up", code = 264_4007, classification = ItemClassification.useful, max_quantity = 15),
|
||||
"Dumbo": KH1ItemData("Summons", code = 264_5000, classification = ItemClassification.progression, ),
|
||||
"Bambi": KH1ItemData("Summons", code = 264_5001, classification = ItemClassification.progression, ),
|
||||
"Genie": KH1ItemData("Summons", code = 264_5002, classification = ItemClassification.progression, ),
|
||||
"Tinker Bell": KH1ItemData("Summons", code = 264_5003, classification = ItemClassification.progression, ),
|
||||
"Mushu": KH1ItemData("Summons", code = 264_5004, classification = ItemClassification.progression, ),
|
||||
"Simba": KH1ItemData("Summons", code = 264_5005, classification = ItemClassification.progression, ),
|
||||
"Progressive Fire": KH1ItemData("Magic", code = 264_6001, classification = ItemClassification.progression, max_quantity = 3 ),
|
||||
"Progressive Blizzard": KH1ItemData("Magic", code = 264_6002, classification = ItemClassification.progression, max_quantity = 3 ),
|
||||
"Progressive Thunder": KH1ItemData("Magic", code = 264_6003, classification = ItemClassification.progression, max_quantity = 3 ),
|
||||
"Progressive Cure": KH1ItemData("Magic", code = 264_6004, classification = ItemClassification.progression, max_quantity = 3 ),
|
||||
"Progressive Gravity": KH1ItemData("Magic", code = 264_6005, classification = ItemClassification.progression, max_quantity = 3 ),
|
||||
"Progressive Stop": KH1ItemData("Magic", code = 264_6006, classification = ItemClassification.progression, max_quantity = 3 ),
|
||||
"Progressive Aero": KH1ItemData("Magic", code = 264_6007, classification = ItemClassification.progression, max_quantity = 3 ),
|
||||
#"Traverse Town": KH1ItemData("Worlds", code = 264_7001, classification = ItemClassification.progression, ),
|
||||
"Wonderland": KH1ItemData("Worlds", code = 264_7002, classification = ItemClassification.progression, ),
|
||||
"Olympus Coliseum": KH1ItemData("Worlds", code = 264_7003, classification = ItemClassification.progression, ),
|
||||
"Deep Jungle": KH1ItemData("Worlds", code = 264_7004, classification = ItemClassification.progression, ),
|
||||
"Agrabah": KH1ItemData("Worlds", code = 264_7005, classification = ItemClassification.progression, ),
|
||||
"Halloween Town": KH1ItemData("Worlds", code = 264_7006, classification = ItemClassification.progression, ),
|
||||
"Atlantica": KH1ItemData("Worlds", code = 264_7007, classification = ItemClassification.progression, ),
|
||||
"Neverland": KH1ItemData("Worlds", code = 264_7008, classification = ItemClassification.progression, ),
|
||||
"Hollow Bastion": KH1ItemData("Worlds", code = 264_7009, classification = ItemClassification.progression, ),
|
||||
"End of the World": KH1ItemData("Worlds", code = 264_7010, classification = ItemClassification.progression, ),
|
||||
"Monstro": KH1ItemData("Worlds", code = 264_7011, classification = ItemClassification.progression, ),
|
||||
"Blue Trinity": KH1ItemData("Trinities", code = 264_8001, classification = ItemClassification.progression, ),
|
||||
"Red Trinity": KH1ItemData("Trinities", code = 264_8002, classification = ItemClassification.progression, ),
|
||||
"Green Trinity": KH1ItemData("Trinities", code = 264_8003, classification = ItemClassification.progression, ),
|
||||
"Yellow Trinity": KH1ItemData("Trinities", code = 264_8004, classification = ItemClassification.progression, ),
|
||||
"White Trinity": KH1ItemData("Trinities", code = 264_8005, classification = ItemClassification.progression, ),
|
||||
"Phil Cup": KH1ItemData("Cups", code = 264_9001, classification = ItemClassification.progression, ),
|
||||
"Pegasus Cup": KH1ItemData("Cups", code = 264_9002, classification = ItemClassification.progression, ),
|
||||
"Hercules Cup": KH1ItemData("Cups", code = 264_9003, classification = ItemClassification.progression, ),
|
||||
#"Hades Cup": KH1ItemData("Cups", code = 264_9004, classification = ItemClassification.progression, ),
|
||||
}
|
||||
|
||||
event_item_table: Dict[str, KH1ItemData] = {}
|
||||
|
||||
#Make item categories
|
||||
item_name_groups: Dict[str, Set[str]] = {}
|
||||
for item in item_table.keys():
|
||||
category = item_table[item].category
|
||||
if category not in item_name_groups.keys():
|
||||
item_name_groups[category] = set()
|
||||
item_name_groups[category].add(item)
|
||||
590
worlds/kh1/Locations.py
Normal file
590
worlds/kh1/Locations.py
Normal file
@@ -0,0 +1,590 @@
|
||||
from typing import Dict, NamedTuple, Optional, Set
|
||||
import typing
|
||||
|
||||
|
||||
from BaseClasses import Location
|
||||
|
||||
|
||||
class KH1Location(Location):
|
||||
game: str = "Kingdom Hearts"
|
||||
|
||||
|
||||
class KH1LocationData(NamedTuple):
|
||||
category: str
|
||||
code: int
|
||||
|
||||
|
||||
def get_locations_by_category(category: str) -> Dict[str, KH1LocationData]:
|
||||
location_dict: Dict[str, KH1LocationData] = {}
|
||||
for name, data in location_table.items():
|
||||
if data.category == category:
|
||||
location_dict.setdefault(name, data)
|
||||
|
||||
return location_dict
|
||||
|
||||
|
||||
location_table: Dict[str, KH1LocationData] = {
|
||||
#"Destiny Islands Chest": KH1LocationData("Destiny Islands", 265_0011), missable
|
||||
"Traverse Town 1st District Candle Puzzle Chest": KH1LocationData("Traverse Town", 265_0211),
|
||||
"Traverse Town 1st District Accessory Shop Roof Chest": KH1LocationData("Traverse Town", 265_0212),
|
||||
"Traverse Town 2nd District Boots and Shoes Awning Chest": KH1LocationData("Traverse Town", 265_0213),
|
||||
"Traverse Town 2nd District Rooftop Chest": KH1LocationData("Traverse Town", 265_0214),
|
||||
"Traverse Town 2nd District Gizmo Shop Facade Chest": KH1LocationData("Traverse Town", 265_0251),
|
||||
"Traverse Town Alleyway Balcony Chest": KH1LocationData("Traverse Town", 265_0252),
|
||||
"Traverse Town Alleyway Blue Room Awning Chest": KH1LocationData("Traverse Town", 265_0253),
|
||||
"Traverse Town Alleyway Corner Chest": KH1LocationData("Traverse Town", 265_0254),
|
||||
"Traverse Town Green Room Clock Puzzle Chest": KH1LocationData("Traverse Town", 265_0292),
|
||||
"Traverse Town Green Room Table Chest": KH1LocationData("Traverse Town", 265_0293),
|
||||
"Traverse Town Red Room Chest": KH1LocationData("Traverse Town", 265_0294),
|
||||
"Traverse Town Mystical House Yellow Trinity Chest": KH1LocationData("Traverse Town", 265_0331),
|
||||
"Traverse Town Accessory Shop Chest": KH1LocationData("Traverse Town", 265_0332),
|
||||
"Traverse Town Secret Waterway White Trinity Chest": KH1LocationData("Traverse Town", 265_0333),
|
||||
"Traverse Town Geppetto's House Chest": KH1LocationData("Traverse Town", 265_0334),
|
||||
"Traverse Town Item Workshop Right Chest": KH1LocationData("Traverse Town", 265_0371),
|
||||
"Traverse Town 1st District Blue Trinity Balcony Chest": KH1LocationData("Traverse Town", 265_0411),
|
||||
"Traverse Town Mystical House Glide Chest": KH1LocationData("Traverse Town", 265_0891),
|
||||
"Traverse Town Alleyway Behind Crates Chest": KH1LocationData("Traverse Town", 265_0892),
|
||||
"Traverse Town Item Workshop Left Chest": KH1LocationData("Traverse Town", 265_0893),
|
||||
"Traverse Town Secret Waterway Near Stairs Chest": KH1LocationData("Traverse Town", 265_0894),
|
||||
"Wonderland Rabbit Hole Green Trinity Chest": KH1LocationData("Wonderland", 265_0931),
|
||||
"Wonderland Rabbit Hole Defeat Heartless 1 Chest": KH1LocationData("Wonderland", 265_0932),
|
||||
"Wonderland Rabbit Hole Defeat Heartless 2 Chest": KH1LocationData("Wonderland", 265_0933),
|
||||
"Wonderland Rabbit Hole Defeat Heartless 3 Chest": KH1LocationData("Wonderland", 265_0934),
|
||||
"Wonderland Bizarre Room Green Trinity Chest": KH1LocationData("Wonderland", 265_0971),
|
||||
"Wonderland Queen's Castle Hedge Left Red Chest": KH1LocationData("Wonderland", 265_1011),
|
||||
"Wonderland Queen's Castle Hedge Right Blue Chest": KH1LocationData("Wonderland", 265_1012),
|
||||
"Wonderland Queen's Castle Hedge Right Red Chest": KH1LocationData("Wonderland", 265_1013),
|
||||
"Wonderland Lotus Forest Thunder Plant Chest": KH1LocationData("Wonderland", 265_1014),
|
||||
"Wonderland Lotus Forest Through the Painting Thunder Plant Chest": KH1LocationData("Wonderland", 265_1051),
|
||||
"Wonderland Lotus Forest Glide Chest": KH1LocationData("Wonderland", 265_1052),
|
||||
"Wonderland Lotus Forest Nut Chest": KH1LocationData("Wonderland", 265_1053),
|
||||
"Wonderland Lotus Forest Corner Chest": KH1LocationData("Wonderland", 265_1054),
|
||||
"Wonderland Bizarre Room Lamp Chest": KH1LocationData("Wonderland", 265_1091),
|
||||
"Wonderland Tea Party Garden Above Lotus Forest Entrance 2nd Chest": KH1LocationData("Wonderland", 265_1093),
|
||||
"Wonderland Tea Party Garden Above Lotus Forest Entrance 1st Chest": KH1LocationData("Wonderland", 265_1094),
|
||||
"Wonderland Tea Party Garden Bear and Clock Puzzle Chest": KH1LocationData("Wonderland", 265_1131),
|
||||
"Wonderland Tea Party Garden Across From Bizarre Room Entrance Chest": KH1LocationData("Wonderland", 265_1132),
|
||||
"Wonderland Lotus Forest Through the Painting White Trinity Chest": KH1LocationData("Wonderland", 265_1133),
|
||||
"Deep Jungle Tree House Beneath Tree House Chest": KH1LocationData("Deep Jungle", 265_1213),
|
||||
"Deep Jungle Tree House Rooftop Chest": KH1LocationData("Deep Jungle", 265_1214),
|
||||
"Deep Jungle Hippo's Lagoon Center Chest": KH1LocationData("Deep Jungle", 265_1251),
|
||||
"Deep Jungle Hippo's Lagoon Left Chest": KH1LocationData("Deep Jungle", 265_1252),
|
||||
"Deep Jungle Hippo's Lagoon Right Chest": KH1LocationData("Deep Jungle", 265_1253),
|
||||
"Deep Jungle Vines Chest": KH1LocationData("Deep Jungle", 265_1291),
|
||||
"Deep Jungle Vines 2 Chest": KH1LocationData("Deep Jungle", 265_1292),
|
||||
"Deep Jungle Climbing Trees Blue Trinity Chest": KH1LocationData("Deep Jungle", 265_1293),
|
||||
"Deep Jungle Tunnel Chest": KH1LocationData("Deep Jungle", 265_1331),
|
||||
"Deep Jungle Cavern of Hearts White Trinity Chest": KH1LocationData("Deep Jungle", 265_1332),
|
||||
"Deep Jungle Camp Blue Trinity Chest": KH1LocationData("Deep Jungle", 265_1333),
|
||||
"Deep Jungle Tent Chest": KH1LocationData("Deep Jungle", 265_1334),
|
||||
"Deep Jungle Waterfall Cavern Low Chest": KH1LocationData("Deep Jungle", 265_1371),
|
||||
"Deep Jungle Waterfall Cavern Middle Chest": KH1LocationData("Deep Jungle", 265_1372),
|
||||
"Deep Jungle Waterfall Cavern High Wall Chest": KH1LocationData("Deep Jungle", 265_1373),
|
||||
"Deep Jungle Waterfall Cavern High Middle Chest": KH1LocationData("Deep Jungle", 265_1374),
|
||||
"Deep Jungle Cliff Right Cliff Left Chest": KH1LocationData("Deep Jungle", 265_1411),
|
||||
"Deep Jungle Cliff Right Cliff Right Chest": KH1LocationData("Deep Jungle", 265_1412),
|
||||
"Deep Jungle Tree House Suspended Boat Chest": KH1LocationData("Deep Jungle", 265_1413),
|
||||
"100 Acre Wood Meadow Inside Log Chest": KH1LocationData("100 Acre Wood", 265_1654),
|
||||
"100 Acre Wood Bouncing Spot Left Cliff Chest": KH1LocationData("100 Acre Wood", 265_1691),
|
||||
"100 Acre Wood Bouncing Spot Right Tree Alcove Chest": KH1LocationData("100 Acre Wood", 265_1692),
|
||||
"100 Acre Wood Bouncing Spot Under Giant Pot Chest": KH1LocationData("100 Acre Wood", 265_1693),
|
||||
"Agrabah Plaza By Storage Chest": KH1LocationData("Agrabah", 265_1972),
|
||||
"Agrabah Plaza Raised Terrace Chest": KH1LocationData("Agrabah", 265_1973),
|
||||
"Agrabah Plaza Top Corner Chest": KH1LocationData("Agrabah", 265_1974),
|
||||
"Agrabah Alley Chest": KH1LocationData("Agrabah", 265_2011),
|
||||
"Agrabah Bazaar Across Windows Chest": KH1LocationData("Agrabah", 265_2012),
|
||||
"Agrabah Bazaar High Corner Chest": KH1LocationData("Agrabah", 265_2013),
|
||||
"Agrabah Main Street Right Palace Entrance Chest": KH1LocationData("Agrabah", 265_2014),
|
||||
"Agrabah Main Street High Above Alley Entrance Chest": KH1LocationData("Agrabah", 265_2051),
|
||||
"Agrabah Main Street High Above Palace Gates Entrance Chest": KH1LocationData("Agrabah", 265_2052),
|
||||
"Agrabah Palace Gates Low Chest": KH1LocationData("Agrabah", 265_2053),
|
||||
"Agrabah Palace Gates High Opposite Palace Chest": KH1LocationData("Agrabah", 265_2054),
|
||||
"Agrabah Palace Gates High Close to Palace Chest": KH1LocationData("Agrabah", 265_2091),
|
||||
"Agrabah Storage Green Trinity Chest": KH1LocationData("Agrabah", 265_2092),
|
||||
"Agrabah Storage Behind Barrel Chest": KH1LocationData("Agrabah", 265_2093),
|
||||
"Agrabah Cave of Wonders Entrance Left Chest": KH1LocationData("Agrabah", 265_2094),
|
||||
"Agrabah Cave of Wonders Entrance Tall Tower Chest": KH1LocationData("Agrabah", 265_2131),
|
||||
"Agrabah Cave of Wonders Hall High Left Chest": KH1LocationData("Agrabah", 265_2132),
|
||||
"Agrabah Cave of Wonders Hall Near Bottomless Hall Chest": KH1LocationData("Agrabah", 265_2133),
|
||||
"Agrabah Cave of Wonders Bottomless Hall Raised Platform Chest": KH1LocationData("Agrabah", 265_2134),
|
||||
"Agrabah Cave of Wonders Bottomless Hall Pillar Chest": KH1LocationData("Agrabah", 265_2171),
|
||||
"Agrabah Cave of Wonders Bottomless Hall Across Chasm Chest": KH1LocationData("Agrabah", 265_2172),
|
||||
"Agrabah Cave of Wonders Treasure Room Across Platforms Chest": KH1LocationData("Agrabah", 265_2173),
|
||||
"Agrabah Cave of Wonders Treasure Room Small Treasure Pile Chest": KH1LocationData("Agrabah", 265_2174),
|
||||
"Agrabah Cave of Wonders Treasure Room Large Treasure Pile Chest": KH1LocationData("Agrabah", 265_2211),
|
||||
"Agrabah Cave of Wonders Treasure Room Above Fire Chest": KH1LocationData("Agrabah", 265_2212),
|
||||
"Agrabah Cave of Wonders Relic Chamber Jump from Stairs Chest": KH1LocationData("Agrabah", 265_2213),
|
||||
"Agrabah Cave of Wonders Relic Chamber Stairs Chest": KH1LocationData("Agrabah", 265_2214),
|
||||
"Agrabah Cave of Wonders Dark Chamber Abu Gem Chest": KH1LocationData("Agrabah", 265_2251),
|
||||
"Agrabah Cave of Wonders Dark Chamber Across from Relic Chamber Entrance Chest": KH1LocationData("Agrabah", 265_2252),
|
||||
"Agrabah Cave of Wonders Dark Chamber Bridge Chest": KH1LocationData("Agrabah", 265_2253),
|
||||
"Agrabah Cave of Wonders Dark Chamber Near Save Chest": KH1LocationData("Agrabah", 265_2254),
|
||||
"Agrabah Cave of Wonders Silent Chamber Blue Trinity Chest": KH1LocationData("Agrabah", 265_2291),
|
||||
"Agrabah Cave of Wonders Hidden Room Right Chest": KH1LocationData("Agrabah", 265_2292),
|
||||
"Agrabah Cave of Wonders Hidden Room Left Chest": KH1LocationData("Agrabah", 265_2293),
|
||||
"Agrabah Aladdin's House Main Street Entrance Chest": KH1LocationData("Agrabah", 265_2294),
|
||||
"Agrabah Aladdin's House Plaza Entrance Chest": KH1LocationData("Agrabah", 265_2331),
|
||||
"Agrabah Cave of Wonders Entrance White Trinity Chest": KH1LocationData("Agrabah", 265_2332),
|
||||
"Monstro Chamber 6 Other Platform Chest": KH1LocationData("Monstro", 265_2413),
|
||||
"Monstro Chamber 6 Platform Near Chamber 5 Entrance Chest": KH1LocationData("Monstro", 265_2414),
|
||||
"Monstro Chamber 6 Raised Area Near Chamber 1 Entrance Chest": KH1LocationData("Monstro", 265_2451),
|
||||
"Monstro Chamber 6 Low Chest": KH1LocationData("Monstro", 265_2452),
|
||||
"Atlantica Sunken Ship In Flipped Boat Chest": KH1LocationData("Atlantica", 265_2531),
|
||||
"Atlantica Sunken Ship Seabed Chest": KH1LocationData("Atlantica", 265_2532),
|
||||
"Atlantica Sunken Ship Inside Ship Chest": KH1LocationData("Atlantica", 265_2533),
|
||||
"Atlantica Ariel's Grotto High Chest": KH1LocationData("Atlantica", 265_2534),
|
||||
"Atlantica Ariel's Grotto Middle Chest": KH1LocationData("Atlantica", 265_2571),
|
||||
"Atlantica Ariel's Grotto Low Chest": KH1LocationData("Atlantica", 265_2572),
|
||||
"Atlantica Ursula's Lair Use Fire on Urchin Chest": KH1LocationData("Atlantica", 265_2573),
|
||||
"Atlantica Undersea Gorge Jammed by Ariel's Grotto Chest": KH1LocationData("Atlantica", 265_2574),
|
||||
"Atlantica Triton's Palace White Trinity Chest": KH1LocationData("Atlantica", 265_2611),
|
||||
"Halloween Town Moonlight Hill White Trinity Chest": KH1LocationData("Halloween Town", 265_3014),
|
||||
"Halloween Town Bridge Under Bridge": KH1LocationData("Halloween Town", 265_3051),
|
||||
"Halloween Town Boneyard Tombstone Puzzle Chest": KH1LocationData("Halloween Town", 265_3052),
|
||||
"Halloween Town Bridge Right of Gate Chest": KH1LocationData("Halloween Town", 265_3053),
|
||||
"Halloween Town Cemetery Behind Grave Chest": KH1LocationData("Halloween Town", 265_3054),
|
||||
"Halloween Town Cemetery By Cat Shape Chest": KH1LocationData("Halloween Town", 265_3091),
|
||||
"Halloween Town Cemetery Between Graves Chest": KH1LocationData("Halloween Town", 265_3092),
|
||||
"Halloween Town Oogie's Manor Lower Iron Cage Chest": KH1LocationData("Halloween Town", 265_3093),
|
||||
"Halloween Town Oogie's Manor Upper Iron Cage Chest": KH1LocationData("Halloween Town", 265_3094),
|
||||
"Halloween Town Oogie's Manor Hollow Chest": KH1LocationData("Halloween Town", 265_3131),
|
||||
"Halloween Town Oogie's Manor Grounds Red Trinity Chest": KH1LocationData("Halloween Town", 265_3132),
|
||||
"Halloween Town Guillotine Square High Tower Chest": KH1LocationData("Halloween Town", 265_3133),
|
||||
"Halloween Town Guillotine Square Pumpkin Structure Left Chest": KH1LocationData("Halloween Town", 265_3134),
|
||||
"Halloween Town Oogie's Manor Entrance Steps Chest": KH1LocationData("Halloween Town", 265_3171),
|
||||
"Halloween Town Oogie's Manor Inside Entrance Chest": KH1LocationData("Halloween Town", 265_3172),
|
||||
"Halloween Town Bridge Left of Gate Chest": KH1LocationData("Halloween Town", 265_3291),
|
||||
"Halloween Town Cemetery By Striped Grave Chest": KH1LocationData("Halloween Town", 265_3292),
|
||||
"Halloween Town Guillotine Square Under Jack's House Stairs Chest": KH1LocationData("Halloween Town", 265_3293),
|
||||
"Halloween Town Guillotine Square Pumpkin Structure Right Chest": KH1LocationData("Halloween Town", 265_3294),
|
||||
"Olympus Coliseum Coliseum Gates Left Behind Columns Chest": KH1LocationData("Olympus Coliseum", 265_3332),
|
||||
"Olympus Coliseum Coliseum Gates Right Blue Trinity Chest": KH1LocationData("Olympus Coliseum", 265_3333),
|
||||
"Olympus Coliseum Coliseum Gates Left Blue Trinity Chest": KH1LocationData("Olympus Coliseum", 265_3334),
|
||||
"Olympus Coliseum Coliseum Gates White Trinity Chest": KH1LocationData("Olympus Coliseum", 265_3371),
|
||||
"Olympus Coliseum Coliseum Gates Blizzara Chest": KH1LocationData("Olympus Coliseum", 265_3372),
|
||||
"Olympus Coliseum Coliseum Gates Blizzaga Chest": KH1LocationData("Olympus Coliseum", 265_3373),
|
||||
"Monstro Mouth Boat Deck Chest": KH1LocationData("Monstro", 265_3454),
|
||||
"Monstro Mouth High Platform Boat Side Chest": KH1LocationData("Monstro", 265_3491),
|
||||
"Monstro Mouth High Platform Across from Boat Chest": KH1LocationData("Monstro", 265_3492),
|
||||
"Monstro Mouth Near Ship Chest": KH1LocationData("Monstro", 265_3493),
|
||||
"Monstro Mouth Green Trinity Top of Boat Chest": KH1LocationData("Monstro", 265_3494),
|
||||
"Monstro Chamber 2 Ground Chest": KH1LocationData("Monstro", 265_3534),
|
||||
"Monstro Chamber 2 Platform Chest": KH1LocationData("Monstro", 265_3571),
|
||||
"Monstro Chamber 5 Platform Chest": KH1LocationData("Monstro", 265_3613),
|
||||
"Monstro Chamber 3 Ground Chest": KH1LocationData("Monstro", 265_3614),
|
||||
"Monstro Chamber 3 Platform Above Chamber 2 Entrance Chest": KH1LocationData("Monstro", 265_3651),
|
||||
"Monstro Chamber 3 Near Chamber 6 Entrance Chest": KH1LocationData("Monstro", 265_3652),
|
||||
"Monstro Chamber 3 Platform Near Chamber 6 Entrance Chest": KH1LocationData("Monstro", 265_3653),
|
||||
"Monstro Mouth High Platform Near Teeth Chest": KH1LocationData("Monstro", 265_3732),
|
||||
"Monstro Chamber 5 Atop Barrel Chest": KH1LocationData("Monstro", 265_3733),
|
||||
"Monstro Chamber 5 Low 2nd Chest": KH1LocationData("Monstro", 265_3734),
|
||||
"Monstro Chamber 5 Low 1st Chest": KH1LocationData("Monstro", 265_3771),
|
||||
"Neverland Pirate Ship Deck White Trinity Chest": KH1LocationData("Neverland", 265_3772),
|
||||
"Neverland Pirate Ship Crows Nest Chest": KH1LocationData("Neverland", 265_3773),
|
||||
"Neverland Hold Yellow Trinity Right Blue Chest": KH1LocationData("Neverland", 265_3774),
|
||||
"Neverland Hold Yellow Trinity Left Blue Chest": KH1LocationData("Neverland", 265_3811),
|
||||
"Neverland Galley Chest": KH1LocationData("Neverland", 265_3812),
|
||||
"Neverland Cabin Chest": KH1LocationData("Neverland", 265_3813),
|
||||
"Neverland Hold Flight 1st Chest": KH1LocationData("Neverland", 265_3814),
|
||||
"Neverland Clock Tower Chest": KH1LocationData("Neverland", 265_4014),
|
||||
"Neverland Hold Flight 2nd Chest": KH1LocationData("Neverland", 265_4051),
|
||||
"Neverland Hold Yellow Trinity Green Chest": KH1LocationData("Neverland", 265_4052),
|
||||
"Neverland Captain's Cabin Chest": KH1LocationData("Neverland", 265_4053),
|
||||
"Hollow Bastion Rising Falls Water's Surface Chest": KH1LocationData("Hollow Bastion", 265_4054),
|
||||
"Hollow Bastion Rising Falls Under Water 1st Chest": KH1LocationData("Hollow Bastion", 265_4091),
|
||||
"Hollow Bastion Rising Falls Under Water 2nd Chest": KH1LocationData("Hollow Bastion", 265_4092),
|
||||
"Hollow Bastion Rising Falls Floating Platform Near Save Chest": KH1LocationData("Hollow Bastion", 265_4093),
|
||||
"Hollow Bastion Rising Falls Floating Platform Near Bubble Chest": KH1LocationData("Hollow Bastion", 265_4094),
|
||||
"Hollow Bastion Rising Falls High Platform Chest": KH1LocationData("Hollow Bastion", 265_4131),
|
||||
"Hollow Bastion Castle Gates Gravity Chest": KH1LocationData("Hollow Bastion", 265_4132),
|
||||
"Hollow Bastion Castle Gates Freestanding Pillar Chest": KH1LocationData("Hollow Bastion", 265_4133),
|
||||
"Hollow Bastion Castle Gates High Pillar Chest": KH1LocationData("Hollow Bastion", 265_4134),
|
||||
"Hollow Bastion Great Crest Lower Chest": KH1LocationData("Hollow Bastion", 265_4171),
|
||||
"Hollow Bastion Great Crest After Battle Platform Chest": KH1LocationData("Hollow Bastion", 265_4172),
|
||||
"Hollow Bastion High Tower 2nd Gravity Chest": KH1LocationData("Hollow Bastion", 265_4173),
|
||||
"Hollow Bastion High Tower 1st Gravity Chest": KH1LocationData("Hollow Bastion", 265_4174),
|
||||
"Hollow Bastion High Tower Above Sliding Blocks Chest": KH1LocationData("Hollow Bastion", 265_4211),
|
||||
"Hollow Bastion Library Top of Bookshelf Chest": KH1LocationData("Hollow Bastion", 265_4213),
|
||||
"Hollow Bastion Library 1st Floor Turn the Carousel Chest": KH1LocationData("Hollow Bastion", 265_4214),
|
||||
"Hollow Bastion Library Top of Bookshelf Turn the Carousel Chest": KH1LocationData("Hollow Bastion", 265_4251),
|
||||
"Hollow Bastion Library 2nd Floor Turn the Carousel 1st Chest": KH1LocationData("Hollow Bastion", 265_4252),
|
||||
"Hollow Bastion Library 2nd Floor Turn the Carousel 2nd Chest": KH1LocationData("Hollow Bastion", 265_4253),
|
||||
"Hollow Bastion Lift Stop Library Node After High Tower Switch Gravity Chest": KH1LocationData("Hollow Bastion", 265_4254),
|
||||
"Hollow Bastion Lift Stop Library Node Gravity Chest": KH1LocationData("Hollow Bastion", 265_4291),
|
||||
"Hollow Bastion Lift Stop Under High Tower Sliding Blocks Chest": KH1LocationData("Hollow Bastion", 265_4292),
|
||||
"Hollow Bastion Lift Stop Outside Library Gravity Chest": KH1LocationData("Hollow Bastion", 265_4293),
|
||||
"Hollow Bastion Lift Stop Heartless Sigil Door Gravity Chest": KH1LocationData("Hollow Bastion", 265_4294),
|
||||
"Hollow Bastion Base Level Bubble Under the Wall Platform Chest": KH1LocationData("Hollow Bastion", 265_4331),
|
||||
"Hollow Bastion Base Level Platform Near Entrance Chest": KH1LocationData("Hollow Bastion", 265_4332),
|
||||
"Hollow Bastion Base Level Near Crystal Switch Chest": KH1LocationData("Hollow Bastion", 265_4333),
|
||||
"Hollow Bastion Waterway Near Save Chest": KH1LocationData("Hollow Bastion", 265_4334),
|
||||
"Hollow Bastion Waterway Blizzard on Bubble Chest": KH1LocationData("Hollow Bastion", 265_4371),
|
||||
"Hollow Bastion Waterway Unlock Passage from Base Level Chest": KH1LocationData("Hollow Bastion", 265_4372),
|
||||
"Hollow Bastion Dungeon By Candles Chest": KH1LocationData("Hollow Bastion", 265_4373),
|
||||
"Hollow Bastion Dungeon Corner Chest": KH1LocationData("Hollow Bastion", 265_4374),
|
||||
"Hollow Bastion Grand Hall Steps Right Side Chest": KH1LocationData("Hollow Bastion", 265_4454),
|
||||
"Hollow Bastion Grand Hall Oblivion Chest": KH1LocationData("Hollow Bastion", 265_4491),
|
||||
"Hollow Bastion Grand Hall Left of Gate Chest": KH1LocationData("Hollow Bastion", 265_4492),
|
||||
#"Hollow Bastion Entrance Hall Push the Statue Chest": KH1LocationData("Hollow Bastion", 265_4493), --handled later
|
||||
"Hollow Bastion Entrance Hall Left of Emblem Door Chest": KH1LocationData("Hollow Bastion", 265_4212),
|
||||
"Hollow Bastion Rising Falls White Trinity Chest": KH1LocationData("Hollow Bastion", 265_4494),
|
||||
"End of the World Final Dimension 1st Chest": KH1LocationData("End of the World", 265_4531),
|
||||
"End of the World Final Dimension 2nd Chest": KH1LocationData("End of the World", 265_4532),
|
||||
"End of the World Final Dimension 3rd Chest": KH1LocationData("End of the World", 265_4533),
|
||||
"End of the World Final Dimension 4th Chest": KH1LocationData("End of the World", 265_4534),
|
||||
"End of the World Final Dimension 5th Chest": KH1LocationData("End of the World", 265_4571),
|
||||
"End of the World Final Dimension 6th Chest": KH1LocationData("End of the World", 265_4572),
|
||||
"End of the World Final Dimension 10th Chest": KH1LocationData("End of the World", 265_4573),
|
||||
"End of the World Final Dimension 9th Chest": KH1LocationData("End of the World", 265_4574),
|
||||
"End of the World Final Dimension 8th Chest": KH1LocationData("End of the World", 265_4611),
|
||||
"End of the World Final Dimension 7th Chest": KH1LocationData("End of the World", 265_4612),
|
||||
"End of the World Giant Crevasse 3rd Chest": KH1LocationData("End of the World", 265_4613),
|
||||
"End of the World Giant Crevasse 5th Chest": KH1LocationData("End of the World", 265_4614),
|
||||
"End of the World Giant Crevasse 1st Chest": KH1LocationData("End of the World", 265_4651),
|
||||
"End of the World Giant Crevasse 4th Chest": KH1LocationData("End of the World", 265_4652),
|
||||
"End of the World Giant Crevasse 2nd Chest": KH1LocationData("End of the World", 265_4653),
|
||||
"End of the World World Terminus Traverse Town Chest": KH1LocationData("End of the World", 265_4654),
|
||||
"End of the World World Terminus Wonderland Chest": KH1LocationData("End of the World", 265_4691),
|
||||
"End of the World World Terminus Olympus Coliseum Chest": KH1LocationData("End of the World", 265_4692),
|
||||
"End of the World World Terminus Deep Jungle Chest": KH1LocationData("End of the World", 265_4693),
|
||||
"End of the World World Terminus Agrabah Chest": KH1LocationData("End of the World", 265_4694),
|
||||
"End of the World World Terminus Atlantica Chest": KH1LocationData("End of the World", 265_4731),
|
||||
"End of the World World Terminus Halloween Town Chest": KH1LocationData("End of the World", 265_4732),
|
||||
"End of the World World Terminus Neverland Chest": KH1LocationData("End of the World", 265_4733),
|
||||
"End of the World World Terminus 100 Acre Wood Chest": KH1LocationData("End of the World", 265_4734),
|
||||
#"End of the World World Terminus Hollow Bastion Chest": KH1LocationData("End of the World", 265_4771),
|
||||
"End of the World Final Rest Chest": KH1LocationData("End of the World", 265_4772),
|
||||
"Monstro Chamber 6 White Trinity Chest": KH1LocationData("End of the World", 265_5092),
|
||||
#"Awakening Chest": KH1LocationData("Awakening", 265_5093), missable
|
||||
|
||||
"Traverse Town Defeat Guard Armor Dodge Roll Event": KH1LocationData("Traverse Town", 265_6011),
|
||||
"Traverse Town Defeat Guard Armor Fire Event": KH1LocationData("Traverse Town", 265_6012),
|
||||
"Traverse Town Defeat Guard Armor Blue Trinity Event": KH1LocationData("Traverse Town", 265_6013),
|
||||
"Traverse Town Leon Secret Waterway Earthshine Event": KH1LocationData("Traverse Town", 265_6014),
|
||||
"Traverse Town Kairi Secret Waterway Oathkeeper Event": KH1LocationData("Traverse Town", 265_6015),
|
||||
"Traverse Town Defeat Guard Armor Brave Warrior Event": KH1LocationData("Traverse Town", 265_6016),
|
||||
"Deep Jungle Defeat Sabor White Fang Event": KH1LocationData("Deep Jungle", 265_6021),
|
||||
"Deep Jungle Defeat Clayton Cure Event": KH1LocationData("Deep Jungle", 265_6022),
|
||||
"Deep Jungle Seal Keyhole Jungle King Event": KH1LocationData("Deep Jungle", 265_6023),
|
||||
"Deep Jungle Seal Keyhole Red Trinity Event": KH1LocationData("Deep Jungle", 265_6024),
|
||||
"Olympus Coliseum Clear Phil's Training Thunder Event": KH1LocationData("Olympus Coliseum", 265_6031),
|
||||
"Olympus Coliseum Defeat Cerberus Inferno Band Event": KH1LocationData("Olympus Coliseum", 265_6033),
|
||||
"Wonderland Defeat Trickmaster Blizzard Event": KH1LocationData("Wonderland", 265_6041),
|
||||
"Wonderland Defeat Trickmaster Ifrit's Horn Event": KH1LocationData("Wonderland", 265_6042),
|
||||
"Agrabah Defeat Pot Centipede Ray of Light Event": KH1LocationData("Agrabah", 265_6051),
|
||||
"Agrabah Defeat Jafar Blizzard Event": KH1LocationData("Agrabah", 265_6052),
|
||||
"Agrabah Defeat Jafar Genie Fire Event": KH1LocationData("Agrabah", 265_6053),
|
||||
"Agrabah Seal Keyhole Genie Event": KH1LocationData("Agrabah", 265_6054),
|
||||
"Agrabah Seal Keyhole Three Wishes Event": KH1LocationData("Agrabah", 265_6055),
|
||||
"Agrabah Seal Keyhole Green Trinity Event": KH1LocationData("Agrabah", 265_6056),
|
||||
"Monstro Defeat Parasite Cage I Goofy Cheer Event": KH1LocationData("Monstro", 265_6061),
|
||||
"Monstro Defeat Parasite Cage II Stop Event": KH1LocationData("Monstro", 265_6062),
|
||||
"Atlantica Defeat Ursula I Mermaid Kick Event": KH1LocationData("Atlantica", 265_6071),
|
||||
"Atlantica Defeat Ursula II Thunder Event": KH1LocationData("Atlantica", 265_6072),
|
||||
"Atlantica Seal Keyhole Crabclaw Event": KH1LocationData("Atlantica", 265_6073),
|
||||
"Halloween Town Defeat Oogie Boogie Holy Circlet Event": KH1LocationData("Halloween Town", 265_6081),
|
||||
"Halloween Town Defeat Oogie's Manor Gravity Event": KH1LocationData("Halloween Town", 265_6082),
|
||||
"Halloween Town Seal Keyhole Pumpkinhead Event": KH1LocationData("Halloween Town", 265_6083),
|
||||
"Neverland Defeat Anti Sora Raven's Claw Event": KH1LocationData("Neverland", 265_6091),
|
||||
"Neverland Encounter Hook Cure Event": KH1LocationData("Neverland", 265_6092),
|
||||
"Neverland Seal Keyhole Fairy Harp Event": KH1LocationData("Neverland", 265_6093),
|
||||
"Neverland Seal Keyhole Tinker Bell Event": KH1LocationData("Neverland", 265_6094),
|
||||
"Neverland Seal Keyhole Glide Event": KH1LocationData("Neverland", 265_6095),
|
||||
"Neverland Defeat Phantom Stop Event": KH1LocationData("Neverland", 265_6096),
|
||||
"Neverland Defeat Captain Hook Ars Arcanum Event": KH1LocationData("Neverland", 265_6097),
|
||||
"Hollow Bastion Defeat Riku I White Trinity Event": KH1LocationData("Hollow Bastion", 265_6101),
|
||||
"Hollow Bastion Defeat Maleficent Donald Cheer Event": KH1LocationData("Hollow Bastion", 265_6102),
|
||||
"Hollow Bastion Defeat Dragon Maleficent Fireglow Event": KH1LocationData("Hollow Bastion", 265_6103),
|
||||
"Hollow Bastion Defeat Riku II Ragnarok Event": KH1LocationData("Hollow Bastion", 265_6104),
|
||||
"Hollow Bastion Defeat Behemoth Omega Arts Event": KH1LocationData("Hollow Bastion", 265_6105),
|
||||
"Hollow Bastion Speak to Princesses Fire Event": KH1LocationData("Hollow Bastion", 265_6106),
|
||||
"End of the World Defeat Chernabog Superglide Event": KH1LocationData("End of the World", 265_6111),
|
||||
|
||||
"Traverse Town Mail Postcard 01 Event": KH1LocationData("Traverse Town", 265_6120),
|
||||
"Traverse Town Mail Postcard 02 Event": KH1LocationData("Traverse Town", 265_6121),
|
||||
"Traverse Town Mail Postcard 03 Event": KH1LocationData("Traverse Town", 265_6122),
|
||||
"Traverse Town Mail Postcard 04 Event": KH1LocationData("Traverse Town", 265_6123),
|
||||
"Traverse Town Mail Postcard 05 Event": KH1LocationData("Traverse Town", 265_6124),
|
||||
"Traverse Town Mail Postcard 06 Event": KH1LocationData("Traverse Town", 265_6125),
|
||||
"Traverse Town Mail Postcard 07 Event": KH1LocationData("Traverse Town", 265_6126),
|
||||
"Traverse Town Mail Postcard 08 Event": KH1LocationData("Traverse Town", 265_6127),
|
||||
"Traverse Town Mail Postcard 09 Event": KH1LocationData("Traverse Town", 265_6128),
|
||||
"Traverse Town Mail Postcard 10 Event": KH1LocationData("Traverse Town", 265_6129),
|
||||
|
||||
"Traverse Town Defeat Opposite Armor Aero Event": KH1LocationData("Traverse Town", 265_6131),
|
||||
|
||||
"Atlantica Undersea Gorge Blizzard Clam": KH1LocationData("Atlantica", 265_6201),
|
||||
"Atlantica Undersea Gorge Ocean Floor Clam": KH1LocationData("Atlantica", 265_6202),
|
||||
"Atlantica Undersea Valley Higher Cave Clam": KH1LocationData("Atlantica", 265_6203),
|
||||
"Atlantica Undersea Valley Lower Cave Clam": KH1LocationData("Atlantica", 265_6204),
|
||||
"Atlantica Undersea Valley Fire Clam": KH1LocationData("Atlantica", 265_6205),
|
||||
"Atlantica Undersea Valley Wall Clam": KH1LocationData("Atlantica", 265_6206),
|
||||
"Atlantica Undersea Valley Pillar Clam": KH1LocationData("Atlantica", 265_6207),
|
||||
"Atlantica Undersea Valley Ocean Floor Clam": KH1LocationData("Atlantica", 265_6208),
|
||||
"Atlantica Triton's Palace Thunder Clam": KH1LocationData("Atlantica", 265_6209),
|
||||
"Atlantica Triton's Palace Wall Right Clam": KH1LocationData("Atlantica", 265_6210),
|
||||
"Atlantica Triton's Palace Near Path Clam": KH1LocationData("Atlantica", 265_6211),
|
||||
"Atlantica Triton's Palace Wall Left Clam": KH1LocationData("Atlantica", 265_6212),
|
||||
"Atlantica Cavern Nook Clam": KH1LocationData("Atlantica", 265_6213),
|
||||
"Atlantica Below Deck Clam": KH1LocationData("Atlantica", 265_6214),
|
||||
"Atlantica Undersea Garden Clam": KH1LocationData("Atlantica", 265_6215),
|
||||
"Atlantica Undersea Cave Clam": KH1LocationData("Atlantica", 265_6216),
|
||||
|
||||
#"Traverse Town Magician's Study Turn in Naturespark": KH1LocationData("Traverse Town", 265_6300),
|
||||
#"Traverse Town Magician's Study Turn in Watergleam": KH1LocationData("Traverse Town", 265_6301),
|
||||
#"Traverse Town Magician's Study Turn in Fireglow": KH1LocationData("Traverse Town", 265_6302),
|
||||
#"Traverse Town Magician's Study Turn in all Summon Gems": KH1LocationData("Traverse Town", 265_6303),
|
||||
"Traverse Town Geppetto's House Geppetto Reward 1": KH1LocationData("Traverse Town", 265_6304),
|
||||
"Traverse Town Geppetto's House Geppetto Reward 2": KH1LocationData("Traverse Town", 265_6305),
|
||||
"Traverse Town Geppetto's House Geppetto Reward 3": KH1LocationData("Traverse Town", 265_6306),
|
||||
"Traverse Town Geppetto's House Geppetto Reward 4": KH1LocationData("Traverse Town", 265_6307),
|
||||
"Traverse Town Geppetto's House Geppetto Reward 5": KH1LocationData("Traverse Town", 265_6308),
|
||||
"Traverse Town Geppetto's House Geppetto All Summons Reward": KH1LocationData("Traverse Town", 265_6309),
|
||||
"Traverse Town Geppetto's House Talk to Pinocchio": KH1LocationData("Traverse Town", 265_6310),
|
||||
"Traverse Town Magician's Study Obtained All Arts Items": KH1LocationData("Traverse Town", 265_6311),
|
||||
"Traverse Town Magician's Study Obtained All LV1 Magic": KH1LocationData("Traverse Town", 265_6312),
|
||||
"Traverse Town Magician's Study Obtained All LV3 Magic": KH1LocationData("Traverse Town", 265_6313),
|
||||
"Traverse Town Piano Room Return 10 Puppies": KH1LocationData("Traverse Town", 265_6314),
|
||||
"Traverse Town Piano Room Return 20 Puppies": KH1LocationData("Traverse Town", 265_6315),
|
||||
"Traverse Town Piano Room Return 30 Puppies": KH1LocationData("Traverse Town", 265_6316),
|
||||
"Traverse Town Piano Room Return 40 Puppies": KH1LocationData("Traverse Town", 265_6317),
|
||||
"Traverse Town Piano Room Return 50 Puppies Reward 1": KH1LocationData("Traverse Town", 265_6318),
|
||||
"Traverse Town Piano Room Return 50 Puppies Reward 2": KH1LocationData("Traverse Town", 265_6319),
|
||||
"Traverse Town Piano Room Return 60 Puppies": KH1LocationData("Traverse Town", 265_6320),
|
||||
"Traverse Town Piano Room Return 70 Puppies": KH1LocationData("Traverse Town", 265_6321),
|
||||
"Traverse Town Piano Room Return 80 Puppies": KH1LocationData("Traverse Town", 265_6322),
|
||||
"Traverse Town Piano Room Return 90 Puppies": KH1LocationData("Traverse Town", 265_6324),
|
||||
"Traverse Town Piano Room Return 99 Puppies Reward 1": KH1LocationData("Traverse Town", 265_6326),
|
||||
"Traverse Town Piano Room Return 99 Puppies Reward 2": KH1LocationData("Traverse Town", 265_6327),
|
||||
"Olympus Coliseum Cloud Sonic Blade Event": KH1LocationData("Olympus Coliseum", 265_6032), #Had to change the way we send this check, not changing location_id
|
||||
"Olympus Coliseum Defeat Sephiroth One-Winged Angel Event": KH1LocationData("Olympus Coliseum", 265_6328),
|
||||
"Olympus Coliseum Defeat Ice Titan Diamond Dust Event": KH1LocationData("Olympus Coliseum", 265_6329),
|
||||
"Olympus Coliseum Gates Purple Jar After Defeating Hades": KH1LocationData("Olympus Coliseum", 265_6330),
|
||||
"Halloween Town Guillotine Square Ring Jack's Doorbell 3 Times": KH1LocationData("Halloween Town", 265_6331),
|
||||
#"Neverland Clock Tower 01:00 Door": KH1LocationData("Neverland", 265_6332),
|
||||
#"Neverland Clock Tower 02:00 Door": KH1LocationData("Neverland", 265_6333),
|
||||
#"Neverland Clock Tower 03:00 Door": KH1LocationData("Neverland", 265_6334),
|
||||
#"Neverland Clock Tower 04:00 Door": KH1LocationData("Neverland", 265_6335),
|
||||
#"Neverland Clock Tower 05:00 Door": KH1LocationData("Neverland", 265_6336),
|
||||
#"Neverland Clock Tower 06:00 Door": KH1LocationData("Neverland", 265_6337),
|
||||
#"Neverland Clock Tower 07:00 Door": KH1LocationData("Neverland", 265_6338),
|
||||
#"Neverland Clock Tower 08:00 Door": KH1LocationData("Neverland", 265_6339),
|
||||
#"Neverland Clock Tower 09:00 Door": KH1LocationData("Neverland", 265_6340),
|
||||
#"Neverland Clock Tower 10:00 Door": KH1LocationData("Neverland", 265_6341),
|
||||
#"Neverland Clock Tower 11:00 Door": KH1LocationData("Neverland", 265_6342),
|
||||
#"Neverland Clock Tower 12:00 Door": KH1LocationData("Neverland", 265_6343),
|
||||
"Neverland Hold Aero Chest": KH1LocationData("Neverland", 265_6344),
|
||||
"100 Acre Wood Bouncing Spot Turn in Rare Nut 1": KH1LocationData("100 Acre Wood", 265_6345),
|
||||
"100 Acre Wood Bouncing Spot Turn in Rare Nut 2": KH1LocationData("100 Acre Wood", 265_6346),
|
||||
"100 Acre Wood Bouncing Spot Turn in Rare Nut 3": KH1LocationData("100 Acre Wood", 265_6347),
|
||||
"100 Acre Wood Bouncing Spot Turn in Rare Nut 4": KH1LocationData("100 Acre Wood", 265_6348),
|
||||
"100 Acre Wood Bouncing Spot Turn in Rare Nut 5": KH1LocationData("100 Acre Wood", 265_6349),
|
||||
"100 Acre Wood Pooh's House Owl Cheer": KH1LocationData("100 Acre Wood", 265_6350),
|
||||
"100 Acre Wood Convert Torn Page 1": KH1LocationData("100 Acre Wood", 265_6351),
|
||||
"100 Acre Wood Convert Torn Page 2": KH1LocationData("100 Acre Wood", 265_6352),
|
||||
"100 Acre Wood Convert Torn Page 3": KH1LocationData("100 Acre Wood", 265_6353),
|
||||
"100 Acre Wood Convert Torn Page 4": KH1LocationData("100 Acre Wood", 265_6354),
|
||||
"100 Acre Wood Convert Torn Page 5": KH1LocationData("100 Acre Wood", 265_6355),
|
||||
"100 Acre Wood Pooh's House Start Fire": KH1LocationData("100 Acre Wood", 265_6356),
|
||||
"100 Acre Wood Pooh's Room Cabinet": KH1LocationData("100 Acre Wood", 265_6357),
|
||||
"100 Acre Wood Pooh's Room Chimney": KH1LocationData("100 Acre Wood", 265_6358),
|
||||
"100 Acre Wood Bouncing Spot Break Log": KH1LocationData("100 Acre Wood", 265_6359),
|
||||
"100 Acre Wood Bouncing Spot Fall Through Top of Tree Next to Pooh": KH1LocationData("100 Acre Wood", 265_6360),
|
||||
"Deep Jungle Camp Hi-Potion Experiment": KH1LocationData("Deep Jungle", 265_6361),
|
||||
"Deep Jungle Camp Ether Experiment": KH1LocationData("Deep Jungle", 265_6362),
|
||||
"Deep Jungle Camp Replication Experiment": KH1LocationData("Deep Jungle", 265_6363),
|
||||
"Deep Jungle Cliff Save Gorillas": KH1LocationData("Deep Jungle", 265_6364),
|
||||
"Deep Jungle Tree House Save Gorillas": KH1LocationData("Deep Jungle", 265_6365),
|
||||
"Deep Jungle Camp Save Gorillas": KH1LocationData("Deep Jungle", 265_6366),
|
||||
"Deep Jungle Bamboo Thicket Save Gorillas": KH1LocationData("Deep Jungle", 265_6367),
|
||||
"Deep Jungle Climbing Trees Save Gorillas": KH1LocationData("Deep Jungle", 265_6368),
|
||||
"Olympus Coliseum Olympia Chest": KH1LocationData("Olympus Coliseum", 265_6369),
|
||||
"Deep Jungle Jungle Slider 10 Fruits": KH1LocationData("Deep Jungle", 265_6370),
|
||||
"Deep Jungle Jungle Slider 20 Fruits": KH1LocationData("Deep Jungle", 265_6371),
|
||||
"Deep Jungle Jungle Slider 30 Fruits": KH1LocationData("Deep Jungle", 265_6372),
|
||||
"Deep Jungle Jungle Slider 40 Fruits": KH1LocationData("Deep Jungle", 265_6373),
|
||||
"Deep Jungle Jungle Slider 50 Fruits": KH1LocationData("Deep Jungle", 265_6374),
|
||||
"Traverse Town 1st District Speak with Cid Event": KH1LocationData("Traverse Town", 265_6375),
|
||||
"Wonderland Bizarre Room Read Book": KH1LocationData("Wonderland", 265_6376),
|
||||
"Olympus Coliseum Coliseum Gates Green Trinity": KH1LocationData("Olympus Coliseum", 265_6377),
|
||||
"Agrabah Defeat Kurt Zisa Zantetsuken Event": KH1LocationData("Agrabah", 265_6378),
|
||||
"Hollow Bastion Defeat Unknown EXP Necklace Event": KH1LocationData("Hollow Bastion", 265_6379),
|
||||
"Olympus Coliseum Coliseum Gates Hero's License Event": KH1LocationData("Olympus Coliseum", 265_6380),
|
||||
"Atlantica Sunken Ship Crystal Trident Event": KH1LocationData("Atlantica", 265_6381),
|
||||
"Halloween Town Graveyard Forget-Me-Not Event": KH1LocationData("Halloween Town", 265_6382),
|
||||
"Deep Jungle Tent Protect-G Event": KH1LocationData("Deep Jungle", 265_6383),
|
||||
"Deep Jungle Cavern of Hearts Navi-G Piece Event": KH1LocationData("Deep Jungle", 265_6384),
|
||||
"Wonderland Bizarre Room Navi-G Piece Event": KH1LocationData("Wonderland", 265_6385),
|
||||
"Olympus Coliseum Coliseum Gates Entry Pass Event": KH1LocationData("Olympus Coliseum", 265_6386),
|
||||
|
||||
"Traverse Town Synth Log": KH1LocationData("Traverse Town", 265_6401),
|
||||
"Traverse Town Synth Cloth": KH1LocationData("Traverse Town", 265_6402),
|
||||
"Traverse Town Synth Rope": KH1LocationData("Traverse Town", 265_6403),
|
||||
"Traverse Town Synth Seagull Egg": KH1LocationData("Traverse Town", 265_6404),
|
||||
"Traverse Town Synth Fish": KH1LocationData("Traverse Town", 265_6405),
|
||||
"Traverse Town Synth Mushroom": KH1LocationData("Traverse Town", 265_6406),
|
||||
|
||||
"Traverse Town Item Shop Postcard": KH1LocationData("Traverse Town", 265_6500),
|
||||
"Traverse Town 1st District Safe Postcard": KH1LocationData("Traverse Town", 265_6501),
|
||||
"Traverse Town Gizmo Shop Postcard 1": KH1LocationData("Traverse Town", 265_6502),
|
||||
"Traverse Town Gizmo Shop Postcard 2": KH1LocationData("Traverse Town", 265_6503),
|
||||
"Traverse Town Item Workshop Postcard": KH1LocationData("Traverse Town", 265_6504),
|
||||
"Traverse Town 3rd District Balcony Postcard": KH1LocationData("Traverse Town", 265_6505),
|
||||
"Traverse Town Geppetto's House Postcard": KH1LocationData("Traverse Town", 265_6506),
|
||||
"Halloween Town Lab Torn Page": KH1LocationData("Halloween Town", 265_6508),
|
||||
"Hollow Bastion Entrance Hall Emblem Piece (Flame)": KH1LocationData("Hollow Bastion", 265_6516),
|
||||
"Hollow Bastion Entrance Hall Emblem Piece (Chest)": KH1LocationData("Hollow Bastion", 265_6517),
|
||||
"Hollow Bastion Entrance Hall Emblem Piece (Statue)": KH1LocationData("Hollow Bastion", 265_6518),
|
||||
"Hollow Bastion Entrance Hall Emblem Piece (Fountain)": KH1LocationData("Hollow Bastion", 265_6519),
|
||||
#"Traverse Town 1st District Leon Gift": KH1LocationData("Traverse Town", 265_6520),
|
||||
#"Traverse Town 1st District Aerith Gift": KH1LocationData("Traverse Town", 265_6521),
|
||||
"Hollow Bastion Library Speak to Belle Divine Rose": KH1LocationData("Hollow Bastion", 265_6522),
|
||||
"Hollow Bastion Library Speak to Aerith Cure": KH1LocationData("Hollow Bastion", 265_6523),
|
||||
|
||||
"Agrabah Defeat Jafar Genie Ansem's Report 1": KH1LocationData("Agrabah", 265_7018),
|
||||
"Hollow Bastion Speak with Aerith Ansem's Report 2": KH1LocationData("Hollow Bastion", 265_7017),
|
||||
"Atlantica Defeat Ursula II Ansem's Report 3": KH1LocationData("Atlantica", 265_7016),
|
||||
"Hollow Bastion Speak with Aerith Ansem's Report 4": KH1LocationData("Hollow Bastion", 265_7015),
|
||||
"Hollow Bastion Defeat Maleficent Ansem's Report 5": KH1LocationData("Hollow Bastion", 265_7014),
|
||||
"Hollow Bastion Speak with Aerith Ansem's Report 6": KH1LocationData("Hollow Bastion", 265_7013),
|
||||
"Halloween Town Defeat Oogie Boogie Ansem's Report 7": KH1LocationData("Halloween Town", 265_7012),
|
||||
"Olympus Coliseum Defeat Hades Ansem's Report 8": KH1LocationData("Olympus Coliseum", 265_7011),
|
||||
"Neverland Defeat Hook Ansem's Report 9": KH1LocationData("Neverland", 265_7028),
|
||||
"Hollow Bastion Speak with Aerith Ansem's Report 10": KH1LocationData("Hollow Bastion", 265_7027),
|
||||
"Agrabah Defeat Kurt Zisa Ansem's Report 11": KH1LocationData("Agrabah", 265_7026),
|
||||
"Olympus Coliseum Defeat Sephiroth Ansem's Report 12": KH1LocationData("Olympus Coliseum", 265_7025),
|
||||
"Hollow Bastion Defeat Unknown Ansem's Report 13": KH1LocationData("Hollow Bastion", 265_7024),
|
||||
"Level 001": KH1LocationData("Levels", 265_8001),
|
||||
"Level 002": KH1LocationData("Levels", 265_8002),
|
||||
"Level 003": KH1LocationData("Levels", 265_8003),
|
||||
"Level 004": KH1LocationData("Levels", 265_8004),
|
||||
"Level 005": KH1LocationData("Levels", 265_8005),
|
||||
"Level 006": KH1LocationData("Levels", 265_8006),
|
||||
"Level 007": KH1LocationData("Levels", 265_8007),
|
||||
"Level 008": KH1LocationData("Levels", 265_8008),
|
||||
"Level 009": KH1LocationData("Levels", 265_8009),
|
||||
"Level 010": KH1LocationData("Levels", 265_8010),
|
||||
"Level 011": KH1LocationData("Levels", 265_8011),
|
||||
"Level 012": KH1LocationData("Levels", 265_8012),
|
||||
"Level 013": KH1LocationData("Levels", 265_8013),
|
||||
"Level 014": KH1LocationData("Levels", 265_8014),
|
||||
"Level 015": KH1LocationData("Levels", 265_8015),
|
||||
"Level 016": KH1LocationData("Levels", 265_8016),
|
||||
"Level 017": KH1LocationData("Levels", 265_8017),
|
||||
"Level 018": KH1LocationData("Levels", 265_8018),
|
||||
"Level 019": KH1LocationData("Levels", 265_8019),
|
||||
"Level 020": KH1LocationData("Levels", 265_8020),
|
||||
"Level 021": KH1LocationData("Levels", 265_8021),
|
||||
"Level 022": KH1LocationData("Levels", 265_8022),
|
||||
"Level 023": KH1LocationData("Levels", 265_8023),
|
||||
"Level 024": KH1LocationData("Levels", 265_8024),
|
||||
"Level 025": KH1LocationData("Levels", 265_8025),
|
||||
"Level 026": KH1LocationData("Levels", 265_8026),
|
||||
"Level 027": KH1LocationData("Levels", 265_8027),
|
||||
"Level 028": KH1LocationData("Levels", 265_8028),
|
||||
"Level 029": KH1LocationData("Levels", 265_8029),
|
||||
"Level 030": KH1LocationData("Levels", 265_8030),
|
||||
"Level 031": KH1LocationData("Levels", 265_8031),
|
||||
"Level 032": KH1LocationData("Levels", 265_8032),
|
||||
"Level 033": KH1LocationData("Levels", 265_8033),
|
||||
"Level 034": KH1LocationData("Levels", 265_8034),
|
||||
"Level 035": KH1LocationData("Levels", 265_8035),
|
||||
"Level 036": KH1LocationData("Levels", 265_8036),
|
||||
"Level 037": KH1LocationData("Levels", 265_8037),
|
||||
"Level 038": KH1LocationData("Levels", 265_8038),
|
||||
"Level 039": KH1LocationData("Levels", 265_8039),
|
||||
"Level 040": KH1LocationData("Levels", 265_8040),
|
||||
"Level 041": KH1LocationData("Levels", 265_8041),
|
||||
"Level 042": KH1LocationData("Levels", 265_8042),
|
||||
"Level 043": KH1LocationData("Levels", 265_8043),
|
||||
"Level 044": KH1LocationData("Levels", 265_8044),
|
||||
"Level 045": KH1LocationData("Levels", 265_8045),
|
||||
"Level 046": KH1LocationData("Levels", 265_8046),
|
||||
"Level 047": KH1LocationData("Levels", 265_8047),
|
||||
"Level 048": KH1LocationData("Levels", 265_8048),
|
||||
"Level 049": KH1LocationData("Levels", 265_8049),
|
||||
"Level 050": KH1LocationData("Levels", 265_8050),
|
||||
"Level 051": KH1LocationData("Levels", 265_8051),
|
||||
"Level 052": KH1LocationData("Levels", 265_8052),
|
||||
"Level 053": KH1LocationData("Levels", 265_8053),
|
||||
"Level 054": KH1LocationData("Levels", 265_8054),
|
||||
"Level 055": KH1LocationData("Levels", 265_8055),
|
||||
"Level 056": KH1LocationData("Levels", 265_8056),
|
||||
"Level 057": KH1LocationData("Levels", 265_8057),
|
||||
"Level 058": KH1LocationData("Levels", 265_8058),
|
||||
"Level 059": KH1LocationData("Levels", 265_8059),
|
||||
"Level 060": KH1LocationData("Levels", 265_8060),
|
||||
"Level 061": KH1LocationData("Levels", 265_8061),
|
||||
"Level 062": KH1LocationData("Levels", 265_8062),
|
||||
"Level 063": KH1LocationData("Levels", 265_8063),
|
||||
"Level 064": KH1LocationData("Levels", 265_8064),
|
||||
"Level 065": KH1LocationData("Levels", 265_8065),
|
||||
"Level 066": KH1LocationData("Levels", 265_8066),
|
||||
"Level 067": KH1LocationData("Levels", 265_8067),
|
||||
"Level 068": KH1LocationData("Levels", 265_8068),
|
||||
"Level 069": KH1LocationData("Levels", 265_8069),
|
||||
"Level 070": KH1LocationData("Levels", 265_8070),
|
||||
"Level 071": KH1LocationData("Levels", 265_8071),
|
||||
"Level 072": KH1LocationData("Levels", 265_8072),
|
||||
"Level 073": KH1LocationData("Levels", 265_8073),
|
||||
"Level 074": KH1LocationData("Levels", 265_8074),
|
||||
"Level 075": KH1LocationData("Levels", 265_8075),
|
||||
"Level 076": KH1LocationData("Levels", 265_8076),
|
||||
"Level 077": KH1LocationData("Levels", 265_8077),
|
||||
"Level 078": KH1LocationData("Levels", 265_8078),
|
||||
"Level 079": KH1LocationData("Levels", 265_8079),
|
||||
"Level 080": KH1LocationData("Levels", 265_8080),
|
||||
"Level 081": KH1LocationData("Levels", 265_8081),
|
||||
"Level 082": KH1LocationData("Levels", 265_8082),
|
||||
"Level 083": KH1LocationData("Levels", 265_8083),
|
||||
"Level 084": KH1LocationData("Levels", 265_8084),
|
||||
"Level 085": KH1LocationData("Levels", 265_8085),
|
||||
"Level 086": KH1LocationData("Levels", 265_8086),
|
||||
"Level 087": KH1LocationData("Levels", 265_8087),
|
||||
"Level 088": KH1LocationData("Levels", 265_8088),
|
||||
"Level 089": KH1LocationData("Levels", 265_8089),
|
||||
"Level 090": KH1LocationData("Levels", 265_8090),
|
||||
"Level 091": KH1LocationData("Levels", 265_8091),
|
||||
"Level 092": KH1LocationData("Levels", 265_8092),
|
||||
"Level 093": KH1LocationData("Levels", 265_8093),
|
||||
"Level 094": KH1LocationData("Levels", 265_8094),
|
||||
"Level 095": KH1LocationData("Levels", 265_8095),
|
||||
"Level 096": KH1LocationData("Levels", 265_8096),
|
||||
"Level 097": KH1LocationData("Levels", 265_8097),
|
||||
"Level 098": KH1LocationData("Levels", 265_8098),
|
||||
"Level 099": KH1LocationData("Levels", 265_8099),
|
||||
"Level 100": KH1LocationData("Levels", 265_8100),
|
||||
"Complete Phil Cup": KH1LocationData("Olympus Coliseum", 265_9001),
|
||||
"Complete Phil Cup Solo": KH1LocationData("Olympus Coliseum", 265_9002),
|
||||
"Complete Phil Cup Time Trial": KH1LocationData("Olympus Coliseum", 265_9003),
|
||||
"Complete Pegasus Cup": KH1LocationData("Olympus Coliseum", 265_9004),
|
||||
"Complete Pegasus Cup Solo": KH1LocationData("Olympus Coliseum", 265_9005),
|
||||
"Complete Pegasus Cup Time Trial": KH1LocationData("Olympus Coliseum", 265_9006),
|
||||
"Complete Hercules Cup": KH1LocationData("Olympus Coliseum", 265_9007),
|
||||
"Complete Hercules Cup Solo": KH1LocationData("Olympus Coliseum", 265_9008),
|
||||
"Complete Hercules Cup Time Trial": KH1LocationData("Olympus Coliseum", 265_9009),
|
||||
"Complete Hades Cup": KH1LocationData("Olympus Coliseum", 265_9010),
|
||||
"Complete Hades Cup Solo": KH1LocationData("Olympus Coliseum", 265_9011),
|
||||
"Complete Hades Cup Time Trial": KH1LocationData("Olympus Coliseum", 265_9012),
|
||||
"Hades Cup Defeat Cloud and Leon Event": KH1LocationData("Olympus Coliseum", 265_9013),
|
||||
"Hades Cup Defeat Yuffie Event": KH1LocationData("Olympus Coliseum", 265_9014),
|
||||
"Hades Cup Defeat Cerberus Event": KH1LocationData("Olympus Coliseum", 265_9015),
|
||||
"Hades Cup Defeat Behemoth Event": KH1LocationData("Olympus Coliseum", 265_9016),
|
||||
"Hades Cup Defeat Hades Event": KH1LocationData("Olympus Coliseum", 265_9017),
|
||||
"Hercules Cup Defeat Cloud Event": KH1LocationData("Olympus Coliseum", 265_9018),
|
||||
"Hercules Cup Yellow Trinity Event": KH1LocationData("Olympus Coliseum", 265_9019),
|
||||
"Final Ansem": KH1LocationData("Final", 265_9999)
|
||||
}
|
||||
|
||||
event_location_table: Dict[str, KH1LocationData] = {}
|
||||
|
||||
lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in location_table.items() if data.code}
|
||||
|
||||
|
||||
#Make location categories
|
||||
location_name_groups: Dict[str, Set[str]] = {}
|
||||
for location in location_table.keys():
|
||||
category = location_table[location].category
|
||||
if category not in location_name_groups.keys():
|
||||
location_name_groups[category] = set()
|
||||
location_name_groups[category].add(location)
|
||||
445
worlds/kh1/Options.py
Normal file
445
worlds/kh1/Options.py
Normal file
@@ -0,0 +1,445 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from Options import NamedRange, Choice, Range, Toggle, DefaultOnToggle, PerGameCommonOptions, StartInventoryPool, OptionGroup
|
||||
|
||||
class StrengthIncrease(Range):
|
||||
"""
|
||||
Determines the number of Strength Increases to add to the multiworld.
|
||||
|
||||
The randomizer will add all stat ups defined here into a pool and choose up to 100 to add to the multiworld.
|
||||
Accessory Slot Increases and Item Slot Increases are prioritized first, then the remaining items (up to 100 total) are chosen at random.
|
||||
"""
|
||||
display_name = "STR Increases"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 24
|
||||
|
||||
class DefenseIncrease(Range):
|
||||
"""
|
||||
Determines the number of Defense Increases to add to the multiworld.
|
||||
|
||||
The randomizer will add all stat ups defined here into a pool and choose up to 100 to add to the multiworld.
|
||||
Accessory Slot Increases and Item Slot Increases are prioritized first, then the remaining items (up to 100 total) are chosen at random.
|
||||
"""
|
||||
display_name = "DEF Increases"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 24
|
||||
|
||||
class HPIncrease(Range):
|
||||
"""
|
||||
Determines the number of HP Increases to add to the multiworld.
|
||||
|
||||
The randomizer will add all stat ups defined here into a pool and choose up to 100 to add to the multiworld.
|
||||
Accessory Slot Increases and Item Slot Increases are prioritized first, then the remaining items (up to 100 total) are chosen at random.
|
||||
"""
|
||||
display_name = "HP Increases"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 23
|
||||
|
||||
class APIncrease(Range):
|
||||
"""
|
||||
Determines the number of AP Increases to add to the multiworld.
|
||||
|
||||
The randomizer will add all stat ups defined here into a pool and choose up to 100 to add to the multiworld.
|
||||
Accessory Slot Increases and Item Slot Increases are prioritized first, then the remaining items (up to 100 total) are chosen at random.
|
||||
"""
|
||||
display_name = "AP Increases"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 18
|
||||
|
||||
class MPIncrease(Range):
|
||||
"""
|
||||
Determines the number of MP Increases to add to the multiworld.
|
||||
|
||||
The randomizer will add all stat ups defined here into a pool and choose up to 100 to add to the multiworld.
|
||||
Accessory Slot Increases and Item Slot Increases are prioritized first, then the remaining items (up to 100 total) are chosen at random.
|
||||
"""
|
||||
display_name = "MP Increases"
|
||||
range_start = 0
|
||||
range_end = 20
|
||||
default = 7
|
||||
|
||||
class AccessorySlotIncrease(Range):
|
||||
"""
|
||||
Determines the number of Accessory Slot Increases to add to the multiworld.
|
||||
|
||||
The randomizer will add all stat ups defined here into a pool and choose up to 100 to add to the multiworld.
|
||||
Accessory Slot Increases and Item Slot Increases are prioritized first, then the remaining items (up to 100 total) are chosen at random.
|
||||
"""
|
||||
display_name = "Accessory Slot Increases"
|
||||
range_start = 0
|
||||
range_end = 6
|
||||
default = 1
|
||||
|
||||
class ItemSlotIncrease(Range):
|
||||
"""
|
||||
Determines the number of Item Slot Increases to add to the multiworld.
|
||||
|
||||
The randomizer will add all stat ups defined here into a pool and choose up to 100 to add to the multiworld.
|
||||
Accessory Slot Increases and Item Slot Increases are prioritized first, then the remaining items (up to 100 total) are chosen at random.
|
||||
"""
|
||||
display_name = "Item Slot Increases"
|
||||
range_start = 0
|
||||
range_end = 5
|
||||
default = 3
|
||||
|
||||
class Atlantica(Toggle):
|
||||
"""
|
||||
Toggle whether to include checks in Atlantica.
|
||||
"""
|
||||
display_name = "Atlantica"
|
||||
|
||||
class HundredAcreWood(Toggle):
|
||||
"""
|
||||
Toggle whether to include checks in the 100 Acre Wood.
|
||||
"""
|
||||
display_name = "100 Acre Wood"
|
||||
|
||||
class SuperBosses(Toggle):
|
||||
"""
|
||||
Toggle whether to include checks behind Super Bosses.
|
||||
"""
|
||||
display_name = "Super Bosses"
|
||||
|
||||
class Cups(Toggle):
|
||||
"""
|
||||
Toggle whether to include checks behind completing Phil, Pegasus, Hercules, or Hades cups.
|
||||
Please note that the cup items will still appear in the multiworld even if toggled off, as they are required to challenge Sephiroth.
|
||||
"""
|
||||
display_name = "Cups"
|
||||
|
||||
class Goal(Choice):
|
||||
"""
|
||||
Determines when victory is achieved in your playthrough.
|
||||
|
||||
Sephiroth: Defeat Sephiroth
|
||||
Unknown: Defeat Unknown
|
||||
Postcards: Turn in all 10 postcards in Traverse Town
|
||||
Final Ansem: Enter End of the World and defeat Ansem as normal
|
||||
Puppies: Rescue and return all 99 puppies in Traverse Town
|
||||
Final Rest: Open the chest in End of the World Final Rest
|
||||
"""
|
||||
display_name = "Goal"
|
||||
option_sephiroth = 0
|
||||
option_unknown = 1
|
||||
option_postcards = 2
|
||||
option_final_ansem = 3
|
||||
option_puppies = 4
|
||||
option_final_rest = 5
|
||||
default = 3
|
||||
|
||||
class EndoftheWorldUnlock(Choice):
|
||||
"""Determines how End of the World is unlocked.
|
||||
|
||||
Item: You can receive an item called "End of the World" which unlocks the world
|
||||
Reports: A certain amount of reports are required to unlock End of the World, which is defined in your options"""
|
||||
display_name = "End of the World Unlock"
|
||||
option_item = 0
|
||||
option_reports = 1
|
||||
default = 1
|
||||
|
||||
class FinalRestDoor(Choice):
|
||||
"""Determines what conditions need to be met to manifest the door in Final Rest, allowing the player to challenge Ansem.
|
||||
|
||||
Reports: A certain number of Ansem's Reports are required, determined by the "Reports to Open Final Rest Door" option
|
||||
Puppies: Having all 99 puppies is required
|
||||
Postcards: Turning in all 10 postcards is required
|
||||
Superbosses: Defeating Sephiroth, Unknown, Kurt Zisa, and Phantom are required
|
||||
"""
|
||||
display_name = "Final Rest Door"
|
||||
option_reports = 0
|
||||
option_puppies = 1
|
||||
option_postcards = 2
|
||||
option_superbosses = 3
|
||||
|
||||
class Puppies(Choice):
|
||||
"""
|
||||
Determines how dalmatian puppies are shuffled into the pool.
|
||||
Full: All puppies are in one location
|
||||
Triplets: Puppies are found in triplets just as they are in the base game
|
||||
Individual: One puppy can be found per location
|
||||
"""
|
||||
display_name = "Puppies"
|
||||
option_full = 0
|
||||
option_triplets = 1
|
||||
option_individual = 2
|
||||
default = 1
|
||||
|
||||
class EXPMultiplier(NamedRange):
|
||||
"""
|
||||
Determines the multiplier to apply to EXP gained.
|
||||
"""
|
||||
display_name = "EXP Multiplier"
|
||||
default = 16
|
||||
range_start = default // 4
|
||||
range_end = 128
|
||||
special_range_names = {
|
||||
"0.25x": int(default // 4),
|
||||
"0.5x": int(default // 2),
|
||||
"1x": default,
|
||||
"2x": default * 2,
|
||||
"3x": default * 3,
|
||||
"4x": default * 4,
|
||||
"8x": default * 8,
|
||||
}
|
||||
|
||||
class RequiredReportsEotW(Range):
|
||||
"""
|
||||
If End of the World Unlock is set to "Reports", determines the number of Ansem's Reports required to open End of the World.
|
||||
"""
|
||||
display_name = "Reports to Open End of the World"
|
||||
default = 4
|
||||
range_start = 0
|
||||
range_end = 13
|
||||
|
||||
class RequiredReportsDoor(Range):
|
||||
"""
|
||||
If Final Rest Door is set to "Reports", determines the number of Ansem's Reports required to manifest the door in Final Rest to challenge Ansem.
|
||||
"""
|
||||
display_name = "Reports to Open Final Rest Door"
|
||||
default = 4
|
||||
range_start = 0
|
||||
range_end = 13
|
||||
|
||||
class ReportsInPool(Range):
|
||||
"""
|
||||
Determines the number of Ansem's Reports in the item pool.
|
||||
"""
|
||||
display_name = "Reports in Pool"
|
||||
default = 4
|
||||
range_start = 0
|
||||
range_end = 13
|
||||
|
||||
class RandomizeKeybladeStats(DefaultOnToggle):
|
||||
"""
|
||||
Determines whether Keyblade stats should be randomized.
|
||||
"""
|
||||
display_name = "Randomize Keyblade Stats"
|
||||
|
||||
class KeybladeMinStrength(Range):
|
||||
"""
|
||||
Determines the minimum STR bonus a keyblade can have.
|
||||
"""
|
||||
display_name = "Keyblade Minimum STR Bonus"
|
||||
default = 3
|
||||
range_start = 0
|
||||
range_end = 20
|
||||
|
||||
class KeybladeMaxStrength(Range):
|
||||
"""
|
||||
Determines the maximum STR bonus a keyblade can have.
|
||||
"""
|
||||
display_name = "Keyblade Maximum STR Bonus"
|
||||
default = 14
|
||||
range_start = 0
|
||||
range_end = 20
|
||||
|
||||
class KeybladeMinMP(Range):
|
||||
"""
|
||||
Determines the minimum MP bonus a keyblade can have.
|
||||
"""
|
||||
display_name = "Keyblade Minimum MP Bonus"
|
||||
default = -2
|
||||
range_start = -2
|
||||
range_end = 5
|
||||
|
||||
class KeybladeMaxMP(Range):
|
||||
"""
|
||||
Determines the maximum MP bonus a keyblade can have.
|
||||
"""
|
||||
display_name = "Keyblade Maximum MP Bonus"
|
||||
default = 3
|
||||
range_start = -2
|
||||
range_end = 5
|
||||
|
||||
class LevelChecks(Range):
|
||||
"""
|
||||
Determines the maximum level for which checks can be obtained.
|
||||
"""
|
||||
display_name = "Level Checks"
|
||||
default = 100
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
|
||||
class ForceStatsOnLevels(NamedRange):
|
||||
"""
|
||||
If this value is less than the value for Level Checks, this determines the minimum level from which only stat ups are obtained at level up locations.
|
||||
For example, if you want to be able to find any multiworld item from levels 1-50, then just stat ups for levels 51-100, set this value to 51.
|
||||
"""
|
||||
display_name = "Force Stats on Level Starting From"
|
||||
default = 1
|
||||
range_start = 1
|
||||
range_end = 101
|
||||
special_range_names = {
|
||||
"none": 101,
|
||||
"multiworld-to-level-50": 51,
|
||||
"all": 1
|
||||
}
|
||||
|
||||
class BadStartingWeapons(Toggle):
|
||||
"""
|
||||
Forces Kingdom Key, Dream Sword, Dream Shield, and Dream Staff to have bad stats.
|
||||
"""
|
||||
display_name = "Bad Starting Weapons"
|
||||
|
||||
class DonaldDeathLink(Toggle):
|
||||
"""
|
||||
If Donald is KO'ed, so is Sora. If Death Link is toggled on in your client, this will send a death to everyone.
|
||||
"""
|
||||
display_name = "Donald Death Link"
|
||||
|
||||
class GoofyDeathLink(Toggle):
|
||||
"""
|
||||
If Goofy is KO'ed, so is Sora. If Death Link is toggled on in your client, this will send a death to everyone.
|
||||
"""
|
||||
display_name = "Goofy Death Link"
|
||||
|
||||
class KeybladesUnlockChests(Toggle):
|
||||
"""
|
||||
If toggled on, the player is required to have a certain keyblade to open chests in certain worlds.
|
||||
TT - Lionheart
|
||||
WL - Lady Luck
|
||||
OC - Olympia
|
||||
DJ - Jungle King
|
||||
AG - Three Wishes
|
||||
MS - Wishing Star
|
||||
HT - Pumpkinhead
|
||||
NL - Fairy Harp
|
||||
HB - Divine Rose
|
||||
EotW - Oblivion
|
||||
HAW - Oathkeeper
|
||||
|
||||
Note: Does not apply to Atlantica, the emblem and carousel chests in Hollow Bastion, or the Aero chest in Neverland currently.
|
||||
"""
|
||||
display_name = "Keyblades Unlock Chests"
|
||||
|
||||
class InteractInBattle(Toggle):
|
||||
"""
|
||||
Allow Sora to talk to people, examine objects, and open chests in battle.
|
||||
"""
|
||||
display_name = "Interact in Battle"
|
||||
|
||||
class AdvancedLogic(Toggle):
|
||||
"""
|
||||
If on, logic may expect you to do advanced skips like using Combo Master, Dumbo, and other unusual methods to reach locations.
|
||||
"""
|
||||
display_name = "Advanced Logic"
|
||||
|
||||
class ExtraSharedAbilities(Toggle):
|
||||
"""
|
||||
If on, adds extra shared abilities to the pool. These can stack, so multiple high jumps make you jump higher and multiple glides make you superglide faster.
|
||||
"""
|
||||
display_name = "Extra Shared Abilities"
|
||||
|
||||
class EXPZeroInPool(Toggle):
|
||||
"""
|
||||
If on, adds EXP Zero ability to the item pool. This is redundant if you are planning on playing on Proud.
|
||||
"""
|
||||
display_name = "EXP Zero in Pool"
|
||||
|
||||
class VanillaEmblemPieces(DefaultOnToggle):
|
||||
"""
|
||||
If on, the Hollow Bastion emblem pieces are in their vanilla locations.
|
||||
"""
|
||||
display_name = "Vanilla Emblem Pieces"
|
||||
|
||||
class StartingWorlds(Range):
|
||||
"""
|
||||
Number of random worlds to start with in addition to Traverse Town, which is always available. Will only consider Atlantica if toggled, and will only consider End of the World if its unlock is set to "Item".
|
||||
"""
|
||||
display_name = "Starting Worlds"
|
||||
default = 0
|
||||
range_start = 0
|
||||
range_end = 10
|
||||
|
||||
@dataclass
|
||||
class KH1Options(PerGameCommonOptions):
|
||||
goal: Goal
|
||||
end_of_the_world_unlock: EndoftheWorldUnlock
|
||||
final_rest_door: FinalRestDoor
|
||||
required_reports_eotw: RequiredReportsEotW
|
||||
required_reports_door: RequiredReportsDoor
|
||||
reports_in_pool: ReportsInPool
|
||||
super_bosses: SuperBosses
|
||||
atlantica: Atlantica
|
||||
hundred_acre_wood: HundredAcreWood
|
||||
cups: Cups
|
||||
puppies: Puppies
|
||||
starting_worlds: StartingWorlds
|
||||
keyblades_unlock_chests: KeybladesUnlockChests
|
||||
interact_in_battle: InteractInBattle
|
||||
exp_multiplier: EXPMultiplier
|
||||
advanced_logic: AdvancedLogic
|
||||
extra_shared_abilities: ExtraSharedAbilities
|
||||
exp_zero_in_pool: EXPZeroInPool
|
||||
vanilla_emblem_pieces: VanillaEmblemPieces
|
||||
donald_death_link: DonaldDeathLink
|
||||
goofy_death_link: GoofyDeathLink
|
||||
randomize_keyblade_stats: RandomizeKeybladeStats
|
||||
bad_starting_weapons: BadStartingWeapons
|
||||
keyblade_min_str: KeybladeMinStrength
|
||||
keyblade_max_str: KeybladeMaxStrength
|
||||
keyblade_min_mp: KeybladeMinMP
|
||||
keyblade_max_mp: KeybladeMaxMP
|
||||
level_checks: LevelChecks
|
||||
force_stats_on_levels: ForceStatsOnLevels
|
||||
strength_increase: StrengthIncrease
|
||||
defense_increase: DefenseIncrease
|
||||
hp_increase: HPIncrease
|
||||
ap_increase: APIncrease
|
||||
mp_increase: MPIncrease
|
||||
accessory_slot_increase: AccessorySlotIncrease
|
||||
item_slot_increase: ItemSlotIncrease
|
||||
start_inventory_from_pool: StartInventoryPool
|
||||
|
||||
kh1_option_groups = [
|
||||
OptionGroup("Goal", [
|
||||
Goal,
|
||||
EndoftheWorldUnlock,
|
||||
FinalRestDoor,
|
||||
RequiredReportsDoor,
|
||||
RequiredReportsEotW,
|
||||
ReportsInPool,
|
||||
]),
|
||||
OptionGroup("Locations", [
|
||||
SuperBosses,
|
||||
Atlantica,
|
||||
Cups,
|
||||
HundredAcreWood,
|
||||
VanillaEmblemPieces,
|
||||
]),
|
||||
OptionGroup("Levels", [
|
||||
EXPMultiplier,
|
||||
LevelChecks,
|
||||
ForceStatsOnLevels,
|
||||
StrengthIncrease,
|
||||
DefenseIncrease,
|
||||
HPIncrease,
|
||||
APIncrease,
|
||||
MPIncrease,
|
||||
AccessorySlotIncrease,
|
||||
ItemSlotIncrease,
|
||||
]),
|
||||
OptionGroup("Keyblades", [
|
||||
KeybladesUnlockChests,
|
||||
RandomizeKeybladeStats,
|
||||
BadStartingWeapons,
|
||||
KeybladeMaxStrength,
|
||||
KeybladeMinStrength,
|
||||
KeybladeMaxMP,
|
||||
KeybladeMinMP,
|
||||
]),
|
||||
OptionGroup("Misc", [
|
||||
StartingWorlds,
|
||||
Puppies,
|
||||
InteractInBattle,
|
||||
AdvancedLogic,
|
||||
ExtraSharedAbilities,
|
||||
EXPZeroInPool,
|
||||
DonaldDeathLink,
|
||||
GoofyDeathLink,
|
||||
])
|
||||
]
|
||||
177
worlds/kh1/Presets.py
Normal file
177
worlds/kh1/Presets.py
Normal file
@@ -0,0 +1,177 @@
|
||||
from typing import Any, Dict
|
||||
|
||||
from .Options import *
|
||||
|
||||
kh1_option_presets: Dict[str, Dict[str, Any]] = {
|
||||
# Standard playthrough where your goal is to defeat Ansem, reaching him by acquiring enough reports.
|
||||
"Final Ansem": {
|
||||
"goal": Goal.option_final_ansem,
|
||||
"end_of_the_world_unlock": EndoftheWorldUnlock.option_reports,
|
||||
"final_rest_door": FinalRestDoor.option_reports,
|
||||
"required_reports_eotw": 7,
|
||||
"required_reports_door": 10,
|
||||
"reports_in_pool": 13,
|
||||
|
||||
"super_bosses": False,
|
||||
"atlantica": False,
|
||||
"hundred_acre_wood": False,
|
||||
"cups": False,
|
||||
"vanilla_emblem_pieces": True,
|
||||
|
||||
"exp_multiplier": 48,
|
||||
"level_checks": 100,
|
||||
"force_stats_on_levels": 1,
|
||||
"strength_increase": 24,
|
||||
"defense_increase": 24,
|
||||
"hp_increase": 23,
|
||||
"ap_increase": 18,
|
||||
"mp_increase": 7,
|
||||
"accessory_slot_increase": 1,
|
||||
"item_slot_increase": 3,
|
||||
|
||||
"keyblades_unlock_chests": False,
|
||||
"randomize_keyblade_stats": True,
|
||||
"bad_starting_weapons": False,
|
||||
"keyblade_max_str": 14,
|
||||
"keyblade_min_str": 3,
|
||||
"keyblade_max_mp": 3,
|
||||
"keyblade_min_mp": -2,
|
||||
|
||||
"puppies": Puppies.option_triplets,
|
||||
"starting_worlds": 0,
|
||||
"interact_in_battle": False,
|
||||
"advanced_logic": False,
|
||||
"extra_shared_abilities": False,
|
||||
"exp_zero_in_pool": False,
|
||||
"donald_death_link": False,
|
||||
"goofy_death_link": False
|
||||
},
|
||||
# Puppies are found individually, and the goal is to return them all.
|
||||
"Puppy Hunt": {
|
||||
"goal": Goal.option_puppies,
|
||||
"end_of_the_world_unlock": EndoftheWorldUnlock.option_item,
|
||||
"final_rest_door": FinalRestDoor.option_puppies,
|
||||
"required_reports_eotw": 13,
|
||||
"required_reports_door": 13,
|
||||
"reports_in_pool": 13,
|
||||
|
||||
"super_bosses": False,
|
||||
"atlantica": False,
|
||||
"hundred_acre_wood": False,
|
||||
"cups": False,
|
||||
"vanilla_emblem_pieces": True,
|
||||
|
||||
"exp_multiplier": 48,
|
||||
"level_checks": 100,
|
||||
"force_stats_on_levels": 1,
|
||||
"strength_increase": 24,
|
||||
"defense_increase": 24,
|
||||
"hp_increase": 23,
|
||||
"ap_increase": 18,
|
||||
"mp_increase": 7,
|
||||
"accessory_slot_increase": 1,
|
||||
"item_slot_increase": 3,
|
||||
|
||||
"keyblades_unlock_chests": False,
|
||||
"randomize_keyblade_stats": True,
|
||||
"bad_starting_weapons": False,
|
||||
"keyblade_max_str": 14,
|
||||
"keyblade_min_str": 3,
|
||||
"keyblade_max_mp": 3,
|
||||
"keyblade_min_mp": -2,
|
||||
|
||||
"puppies": Puppies.option_individual,
|
||||
"starting_worlds": 0,
|
||||
"interact_in_battle": False,
|
||||
"advanced_logic": False,
|
||||
"extra_shared_abilities": False,
|
||||
"exp_zero_in_pool": False,
|
||||
"donald_death_link": False,
|
||||
"goofy_death_link": False
|
||||
},
|
||||
# Advanced playthrough with most settings on.
|
||||
"Advanced": {
|
||||
"goal": Goal.option_final_ansem,
|
||||
"end_of_the_world_unlock": EndoftheWorldUnlock.option_reports,
|
||||
"final_rest_door": FinalRestDoor.option_reports,
|
||||
"required_reports_eotw": 7,
|
||||
"required_reports_door": 10,
|
||||
"reports_in_pool": 13,
|
||||
|
||||
"super_bosses": True,
|
||||
"atlantica": True,
|
||||
"hundred_acre_wood": True,
|
||||
"cups": True,
|
||||
"vanilla_emblem_pieces": False,
|
||||
|
||||
"exp_multiplier": 48,
|
||||
"level_checks": 100,
|
||||
"force_stats_on_levels": 1,
|
||||
"strength_increase": 24,
|
||||
"defense_increase": 24,
|
||||
"hp_increase": 23,
|
||||
"ap_increase": 18,
|
||||
"mp_increase": 7,
|
||||
"accessory_slot_increase": 1,
|
||||
"item_slot_increase": 3,
|
||||
|
||||
"keyblades_unlock_chests": True,
|
||||
"randomize_keyblade_stats": True,
|
||||
"bad_starting_weapons": True,
|
||||
"keyblade_max_str": 14,
|
||||
"keyblade_min_str": 3,
|
||||
"keyblade_max_mp": 3,
|
||||
"keyblade_min_mp": -2,
|
||||
|
||||
"puppies": Puppies.option_triplets,
|
||||
"starting_worlds": 0,
|
||||
"interact_in_battle": True,
|
||||
"advanced_logic": True,
|
||||
"extra_shared_abilities": True,
|
||||
"exp_zero_in_pool": True,
|
||||
"donald_death_link": False,
|
||||
"goofy_death_link": False
|
||||
},
|
||||
# Playthrough meant to enhance the level 1 experience.
|
||||
"Level 1": {
|
||||
"goal": Goal.option_final_ansem,
|
||||
"end_of_the_world_unlock": EndoftheWorldUnlock.option_reports,
|
||||
"final_rest_door": FinalRestDoor.option_reports,
|
||||
"required_reports_eotw": 7,
|
||||
"required_reports_door": 10,
|
||||
"reports_in_pool": 13,
|
||||
|
||||
"super_bosses": False,
|
||||
"atlantica": False,
|
||||
"hundred_acre_wood": False,
|
||||
"cups": False,
|
||||
"vanilla_emblem_pieces": True,
|
||||
|
||||
"exp_multiplier": 16,
|
||||
"level_checks": 0,
|
||||
"force_stats_on_levels": 101,
|
||||
"strength_increase": 0,
|
||||
"defense_increase": 0,
|
||||
"hp_increase": 0,
|
||||
"mp_increase": 0,
|
||||
"accessory_slot_increase": 6,
|
||||
"item_slot_increase": 5,
|
||||
|
||||
"keyblades_unlock_chests": False,
|
||||
"randomize_keyblade_stats": True,
|
||||
"bad_starting_weapons": False,
|
||||
"keyblade_max_str": 14,
|
||||
"keyblade_min_str": 3,
|
||||
"keyblade_max_mp": 3,
|
||||
"keyblade_min_mp": -2,
|
||||
|
||||
"puppies": Puppies.option_triplets,
|
||||
"starting_worlds": 0,
|
||||
"interact_in_battle": False,
|
||||
"advanced_logic": False,
|
||||
"extra_shared_abilities": False,
|
||||
"exp_zero_in_pool": False,
|
||||
"donald_death_link": False,
|
||||
"goofy_death_link": False
|
||||
}
|
||||
}
|
||||
516
worlds/kh1/Regions.py
Normal file
516
worlds/kh1/Regions.py
Normal file
@@ -0,0 +1,516 @@
|
||||
from typing import Dict, List, NamedTuple, Optional
|
||||
|
||||
from BaseClasses import MultiWorld, Region, Entrance
|
||||
from .Locations import KH1Location, location_table
|
||||
|
||||
|
||||
class KH1RegionData(NamedTuple):
|
||||
locations: List[str]
|
||||
region_exits: Optional[List[str]]
|
||||
|
||||
|
||||
def create_regions(multiworld: MultiWorld, player: int, options):
|
||||
regions: Dict[str, KH1RegionData] = {
|
||||
"Menu": KH1RegionData([], ["Awakening", "Levels"]),
|
||||
"Awakening": KH1RegionData([], ["Destiny Islands"]),
|
||||
"Destiny Islands": KH1RegionData([], ["Traverse Town"]),
|
||||
"Traverse Town": KH1RegionData([], ["World Map"]),
|
||||
"Wonderland": KH1RegionData([], []),
|
||||
"Olympus Coliseum": KH1RegionData([], []),
|
||||
"Deep Jungle": KH1RegionData([], []),
|
||||
"Agrabah": KH1RegionData([], []),
|
||||
"Monstro": KH1RegionData([], []),
|
||||
"Atlantica": KH1RegionData([], []),
|
||||
"Halloween Town": KH1RegionData([], []),
|
||||
"Neverland": KH1RegionData([], []),
|
||||
"Hollow Bastion": KH1RegionData([], []),
|
||||
"End of the World": KH1RegionData([], []),
|
||||
"100 Acre Wood": KH1RegionData([], []),
|
||||
"Levels": KH1RegionData([], []),
|
||||
"World Map": KH1RegionData([], ["Wonderland", "Olympus Coliseum", "Deep Jungle",
|
||||
"Agrabah", "Monstro", "Atlantica",
|
||||
"Halloween Town", "Neverland", "Hollow Bastion",
|
||||
"End of the World", "100 Acre Wood"])
|
||||
}
|
||||
|
||||
# Set up locations
|
||||
regions["Agrabah"].locations.append("Agrabah Aladdin's House Main Street Entrance Chest")
|
||||
regions["Agrabah"].locations.append("Agrabah Aladdin's House Plaza Entrance Chest")
|
||||
regions["Agrabah"].locations.append("Agrabah Alley Chest")
|
||||
regions["Agrabah"].locations.append("Agrabah Bazaar Across Windows Chest")
|
||||
regions["Agrabah"].locations.append("Agrabah Bazaar High Corner Chest")
|
||||
regions["Agrabah"].locations.append("Agrabah Cave of Wonders Bottomless Hall Across Chasm Chest")
|
||||
regions["Agrabah"].locations.append("Agrabah Cave of Wonders Bottomless Hall Pillar Chest")
|
||||
regions["Agrabah"].locations.append("Agrabah Cave of Wonders Bottomless Hall Raised Platform Chest")
|
||||
regions["Agrabah"].locations.append("Agrabah Cave of Wonders Dark Chamber Abu Gem Chest")
|
||||
regions["Agrabah"].locations.append("Agrabah Cave of Wonders Dark Chamber Across from Relic Chamber Entrance Chest")
|
||||
regions["Agrabah"].locations.append("Agrabah Cave of Wonders Dark Chamber Bridge Chest")
|
||||
regions["Agrabah"].locations.append("Agrabah Cave of Wonders Dark Chamber Near Save Chest")
|
||||
regions["Agrabah"].locations.append("Agrabah Cave of Wonders Entrance Left Chest")
|
||||
regions["Agrabah"].locations.append("Agrabah Cave of Wonders Entrance Tall Tower Chest")
|
||||
regions["Agrabah"].locations.append("Agrabah Cave of Wonders Entrance White Trinity Chest")
|
||||
regions["Agrabah"].locations.append("Agrabah Cave of Wonders Hall High Left Chest")
|
||||
regions["Agrabah"].locations.append("Agrabah Cave of Wonders Hall Near Bottomless Hall Chest")
|
||||
regions["Agrabah"].locations.append("Agrabah Cave of Wonders Hidden Room Left Chest")
|
||||
regions["Agrabah"].locations.append("Agrabah Cave of Wonders Hidden Room Right Chest")
|
||||
regions["Agrabah"].locations.append("Agrabah Cave of Wonders Relic Chamber Jump from Stairs Chest")
|
||||
regions["Agrabah"].locations.append("Agrabah Cave of Wonders Relic Chamber Stairs Chest")
|
||||
regions["Agrabah"].locations.append("Agrabah Cave of Wonders Silent Chamber Blue Trinity Chest")
|
||||
regions["Agrabah"].locations.append("Agrabah Cave of Wonders Treasure Room Above Fire Chest")
|
||||
regions["Agrabah"].locations.append("Agrabah Cave of Wonders Treasure Room Across Platforms Chest")
|
||||
regions["Agrabah"].locations.append("Agrabah Cave of Wonders Treasure Room Large Treasure Pile Chest")
|
||||
regions["Agrabah"].locations.append("Agrabah Cave of Wonders Treasure Room Small Treasure Pile Chest")
|
||||
regions["Agrabah"].locations.append("Agrabah Defeat Jafar Blizzard Event")
|
||||
regions["Agrabah"].locations.append("Agrabah Defeat Jafar Genie Ansem's Report 1")
|
||||
regions["Agrabah"].locations.append("Agrabah Defeat Jafar Genie Fire Event")
|
||||
regions["Agrabah"].locations.append("Agrabah Defeat Pot Centipede Ray of Light Event")
|
||||
regions["Agrabah"].locations.append("Agrabah Main Street High Above Alley Entrance Chest")
|
||||
regions["Agrabah"].locations.append("Agrabah Main Street High Above Palace Gates Entrance Chest")
|
||||
regions["Agrabah"].locations.append("Agrabah Main Street Right Palace Entrance Chest")
|
||||
regions["Agrabah"].locations.append("Agrabah Palace Gates High Close to Palace Chest")
|
||||
regions["Agrabah"].locations.append("Agrabah Palace Gates High Opposite Palace Chest")
|
||||
regions["Agrabah"].locations.append("Agrabah Palace Gates Low Chest")
|
||||
regions["Agrabah"].locations.append("Agrabah Plaza By Storage Chest")
|
||||
regions["Agrabah"].locations.append("Agrabah Plaza Raised Terrace Chest")
|
||||
regions["Agrabah"].locations.append("Agrabah Plaza Top Corner Chest")
|
||||
regions["Agrabah"].locations.append("Agrabah Seal Keyhole Genie Event")
|
||||
regions["Agrabah"].locations.append("Agrabah Seal Keyhole Green Trinity Event")
|
||||
regions["Agrabah"].locations.append("Agrabah Seal Keyhole Three Wishes Event")
|
||||
regions["Agrabah"].locations.append("Agrabah Storage Behind Barrel Chest")
|
||||
regions["Agrabah"].locations.append("Agrabah Storage Green Trinity Chest")
|
||||
regions["Deep Jungle"].locations.append("Deep Jungle Bamboo Thicket Save Gorillas")
|
||||
regions["Deep Jungle"].locations.append("Deep Jungle Camp Blue Trinity Chest")
|
||||
regions["Deep Jungle"].locations.append("Deep Jungle Camp Ether Experiment")
|
||||
regions["Deep Jungle"].locations.append("Deep Jungle Camp Hi-Potion Experiment")
|
||||
regions["Deep Jungle"].locations.append("Deep Jungle Camp Replication Experiment")
|
||||
regions["Deep Jungle"].locations.append("Deep Jungle Camp Save Gorillas")
|
||||
regions["Deep Jungle"].locations.append("Deep Jungle Cavern of Hearts Navi-G Piece Event")
|
||||
regions["Deep Jungle"].locations.append("Deep Jungle Cavern of Hearts White Trinity Chest")
|
||||
regions["Deep Jungle"].locations.append("Deep Jungle Cliff Right Cliff Left Chest")
|
||||
regions["Deep Jungle"].locations.append("Deep Jungle Cliff Right Cliff Right Chest")
|
||||
regions["Deep Jungle"].locations.append("Deep Jungle Cliff Save Gorillas")
|
||||
regions["Deep Jungle"].locations.append("Deep Jungle Climbing Trees Blue Trinity Chest")
|
||||
regions["Deep Jungle"].locations.append("Deep Jungle Climbing Trees Save Gorillas")
|
||||
regions["Deep Jungle"].locations.append("Deep Jungle Defeat Clayton Cure Event")
|
||||
regions["Deep Jungle"].locations.append("Deep Jungle Defeat Sabor White Fang Event")
|
||||
regions["Deep Jungle"].locations.append("Deep Jungle Hippo's Lagoon Center Chest")
|
||||
regions["Deep Jungle"].locations.append("Deep Jungle Hippo's Lagoon Left Chest")
|
||||
regions["Deep Jungle"].locations.append("Deep Jungle Hippo's Lagoon Right Chest")
|
||||
regions["Deep Jungle"].locations.append("Deep Jungle Jungle Slider 10 Fruits")
|
||||
regions["Deep Jungle"].locations.append("Deep Jungle Jungle Slider 20 Fruits")
|
||||
regions["Deep Jungle"].locations.append("Deep Jungle Jungle Slider 30 Fruits")
|
||||
regions["Deep Jungle"].locations.append("Deep Jungle Jungle Slider 40 Fruits")
|
||||
regions["Deep Jungle"].locations.append("Deep Jungle Jungle Slider 50 Fruits")
|
||||
regions["Deep Jungle"].locations.append("Deep Jungle Seal Keyhole Jungle King Event")
|
||||
regions["Deep Jungle"].locations.append("Deep Jungle Seal Keyhole Red Trinity Event")
|
||||
regions["Deep Jungle"].locations.append("Deep Jungle Tent Chest")
|
||||
regions["Deep Jungle"].locations.append("Deep Jungle Tent Protect-G Event")
|
||||
regions["Deep Jungle"].locations.append("Deep Jungle Tree House Beneath Tree House Chest")
|
||||
regions["Deep Jungle"].locations.append("Deep Jungle Tree House Rooftop Chest")
|
||||
regions["Deep Jungle"].locations.append("Deep Jungle Tree House Save Gorillas")
|
||||
regions["Deep Jungle"].locations.append("Deep Jungle Tree House Suspended Boat Chest")
|
||||
regions["Deep Jungle"].locations.append("Deep Jungle Tunnel Chest")
|
||||
regions["Deep Jungle"].locations.append("Deep Jungle Vines 2 Chest")
|
||||
regions["Deep Jungle"].locations.append("Deep Jungle Vines Chest")
|
||||
regions["Deep Jungle"].locations.append("Deep Jungle Waterfall Cavern High Middle Chest")
|
||||
regions["Deep Jungle"].locations.append("Deep Jungle Waterfall Cavern High Wall Chest")
|
||||
regions["Deep Jungle"].locations.append("Deep Jungle Waterfall Cavern Low Chest")
|
||||
regions["Deep Jungle"].locations.append("Deep Jungle Waterfall Cavern Middle Chest")
|
||||
regions["End of the World"].locations.append("End of the World Defeat Chernabog Superglide Event")
|
||||
regions["End of the World"].locations.append("End of the World Final Dimension 10th Chest")
|
||||
regions["End of the World"].locations.append("End of the World Final Dimension 1st Chest")
|
||||
regions["End of the World"].locations.append("End of the World Final Dimension 2nd Chest")
|
||||
regions["End of the World"].locations.append("End of the World Final Dimension 3rd Chest")
|
||||
regions["End of the World"].locations.append("End of the World Final Dimension 4th Chest")
|
||||
regions["End of the World"].locations.append("End of the World Final Dimension 5th Chest")
|
||||
regions["End of the World"].locations.append("End of the World Final Dimension 6th Chest")
|
||||
regions["End of the World"].locations.append("End of the World Final Dimension 7th Chest")
|
||||
regions["End of the World"].locations.append("End of the World Final Dimension 8th Chest")
|
||||
regions["End of the World"].locations.append("End of the World Final Dimension 9th Chest")
|
||||
regions["End of the World"].locations.append("End of the World Final Rest Chest")
|
||||
regions["End of the World"].locations.append("End of the World Giant Crevasse 1st Chest")
|
||||
regions["End of the World"].locations.append("End of the World Giant Crevasse 2nd Chest")
|
||||
regions["End of the World"].locations.append("End of the World Giant Crevasse 3rd Chest")
|
||||
regions["End of the World"].locations.append("End of the World Giant Crevasse 4th Chest")
|
||||
regions["End of the World"].locations.append("End of the World Giant Crevasse 5th Chest")
|
||||
regions["End of the World"].locations.append("End of the World World Terminus 100 Acre Wood Chest")
|
||||
regions["End of the World"].locations.append("End of the World World Terminus Agrabah Chest")
|
||||
regions["End of the World"].locations.append("End of the World World Terminus Atlantica Chest")
|
||||
regions["End of the World"].locations.append("End of the World World Terminus Deep Jungle Chest")
|
||||
regions["End of the World"].locations.append("End of the World World Terminus Halloween Town Chest")
|
||||
#regions["End of the World"].locations.append("End of the World World Terminus Hollow Bastion Chest")
|
||||
regions["End of the World"].locations.append("End of the World World Terminus Neverland Chest")
|
||||
regions["End of the World"].locations.append("End of the World World Terminus Olympus Coliseum Chest")
|
||||
regions["End of the World"].locations.append("End of the World World Terminus Traverse Town Chest")
|
||||
regions["End of the World"].locations.append("End of the World World Terminus Wonderland Chest")
|
||||
regions["Halloween Town"].locations.append("Halloween Town Boneyard Tombstone Puzzle Chest")
|
||||
regions["Halloween Town"].locations.append("Halloween Town Bridge Left of Gate Chest")
|
||||
regions["Halloween Town"].locations.append("Halloween Town Bridge Right of Gate Chest")
|
||||
regions["Halloween Town"].locations.append("Halloween Town Bridge Under Bridge")
|
||||
regions["Halloween Town"].locations.append("Halloween Town Cemetery Behind Grave Chest")
|
||||
regions["Halloween Town"].locations.append("Halloween Town Cemetery Between Graves Chest")
|
||||
regions["Halloween Town"].locations.append("Halloween Town Cemetery By Cat Shape Chest")
|
||||
regions["Halloween Town"].locations.append("Halloween Town Cemetery By Striped Grave Chest")
|
||||
regions["Halloween Town"].locations.append("Halloween Town Defeat Oogie Boogie Ansem's Report 7")
|
||||
regions["Halloween Town"].locations.append("Halloween Town Defeat Oogie Boogie Holy Circlet Event")
|
||||
regions["Halloween Town"].locations.append("Halloween Town Defeat Oogie's Manor Gravity Event")
|
||||
regions["Halloween Town"].locations.append("Halloween Town Graveyard Forget-Me-Not Event")
|
||||
regions["Halloween Town"].locations.append("Halloween Town Guillotine Square High Tower Chest")
|
||||
regions["Halloween Town"].locations.append("Halloween Town Guillotine Square Pumpkin Structure Left Chest")
|
||||
regions["Halloween Town"].locations.append("Halloween Town Guillotine Square Pumpkin Structure Right Chest")
|
||||
regions["Halloween Town"].locations.append("Halloween Town Guillotine Square Ring Jack's Doorbell 3 Times")
|
||||
regions["Halloween Town"].locations.append("Halloween Town Guillotine Square Under Jack's House Stairs Chest")
|
||||
regions["Halloween Town"].locations.append("Halloween Town Lab Torn Page")
|
||||
regions["Halloween Town"].locations.append("Halloween Town Moonlight Hill White Trinity Chest")
|
||||
regions["Halloween Town"].locations.append("Halloween Town Oogie's Manor Entrance Steps Chest")
|
||||
regions["Halloween Town"].locations.append("Halloween Town Oogie's Manor Grounds Red Trinity Chest")
|
||||
regions["Halloween Town"].locations.append("Halloween Town Oogie's Manor Hollow Chest")
|
||||
regions["Halloween Town"].locations.append("Halloween Town Oogie's Manor Inside Entrance Chest")
|
||||
regions["Halloween Town"].locations.append("Halloween Town Oogie's Manor Lower Iron Cage Chest")
|
||||
regions["Halloween Town"].locations.append("Halloween Town Oogie's Manor Upper Iron Cage Chest")
|
||||
regions["Halloween Town"].locations.append("Halloween Town Seal Keyhole Pumpkinhead Event")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Base Level Bubble Under the Wall Platform Chest")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Base Level Near Crystal Switch Chest")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Base Level Platform Near Entrance Chest")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Castle Gates Freestanding Pillar Chest")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Castle Gates Gravity Chest")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Castle Gates High Pillar Chest")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Defeat Behemoth Omega Arts Event")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Defeat Dragon Maleficent Fireglow Event")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Defeat Maleficent Ansem's Report 5")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Defeat Maleficent Donald Cheer Event")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Defeat Riku I White Trinity Event")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Defeat Riku II Ragnarok Event")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Dungeon By Candles Chest")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Dungeon Corner Chest")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Entrance Hall Emblem Piece (Chest)")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Entrance Hall Emblem Piece (Flame)")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Entrance Hall Emblem Piece (Fountain)")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Entrance Hall Emblem Piece (Statue)")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Entrance Hall Left of Emblem Door Chest")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Grand Hall Left of Gate Chest")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Grand Hall Oblivion Chest")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Grand Hall Steps Right Side Chest")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Great Crest After Battle Platform Chest")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Great Crest Lower Chest")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion High Tower 1st Gravity Chest")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion High Tower 2nd Gravity Chest")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion High Tower Above Sliding Blocks Chest")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Library 1st Floor Turn the Carousel Chest")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Library 2nd Floor Turn the Carousel 1st Chest")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Library 2nd Floor Turn the Carousel 2nd Chest")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Library Speak to Aerith Cure")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Library Speak to Belle Divine Rose")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Library Top of Bookshelf Chest")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Library Top of Bookshelf Turn the Carousel Chest")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Lift Stop Heartless Sigil Door Gravity Chest")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Lift Stop Library Node After High Tower Switch Gravity Chest")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Lift Stop Library Node Gravity Chest")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Lift Stop Outside Library Gravity Chest")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Lift Stop Under High Tower Sliding Blocks Chest")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Rising Falls Floating Platform Near Bubble Chest")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Rising Falls Floating Platform Near Save Chest")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Rising Falls High Platform Chest")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Rising Falls Under Water 1st Chest")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Rising Falls Under Water 2nd Chest")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Rising Falls Water's Surface Chest")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Rising Falls White Trinity Chest")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Speak to Princesses Fire Event")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Speak with Aerith Ansem's Report 10")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Speak with Aerith Ansem's Report 2")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Speak with Aerith Ansem's Report 4")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Speak with Aerith Ansem's Report 6")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Waterway Blizzard on Bubble Chest")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Waterway Near Save Chest")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Waterway Unlock Passage from Base Level Chest")
|
||||
regions["Monstro"].locations.append("Monstro Chamber 2 Ground Chest")
|
||||
regions["Monstro"].locations.append("Monstro Chamber 2 Platform Chest")
|
||||
regions["Monstro"].locations.append("Monstro Chamber 3 Ground Chest")
|
||||
regions["Monstro"].locations.append("Monstro Chamber 3 Near Chamber 6 Entrance Chest")
|
||||
regions["Monstro"].locations.append("Monstro Chamber 3 Platform Above Chamber 2 Entrance Chest")
|
||||
regions["Monstro"].locations.append("Monstro Chamber 3 Platform Near Chamber 6 Entrance Chest")
|
||||
regions["Monstro"].locations.append("Monstro Chamber 5 Atop Barrel Chest")
|
||||
regions["Monstro"].locations.append("Monstro Chamber 5 Low 1st Chest")
|
||||
regions["Monstro"].locations.append("Monstro Chamber 5 Low 2nd Chest")
|
||||
regions["Monstro"].locations.append("Monstro Chamber 5 Platform Chest")
|
||||
regions["Monstro"].locations.append("Monstro Chamber 6 Low Chest")
|
||||
regions["Monstro"].locations.append("Monstro Chamber 6 Other Platform Chest")
|
||||
regions["Monstro"].locations.append("Monstro Chamber 6 Platform Near Chamber 5 Entrance Chest")
|
||||
regions["Monstro"].locations.append("Monstro Chamber 6 Raised Area Near Chamber 1 Entrance Chest")
|
||||
regions["Monstro"].locations.append("Monstro Chamber 6 White Trinity Chest")
|
||||
regions["Monstro"].locations.append("Monstro Defeat Parasite Cage I Goofy Cheer Event")
|
||||
regions["Monstro"].locations.append("Monstro Defeat Parasite Cage II Stop Event")
|
||||
regions["Monstro"].locations.append("Monstro Mouth Boat Deck Chest")
|
||||
regions["Monstro"].locations.append("Monstro Mouth Green Trinity Top of Boat Chest")
|
||||
regions["Monstro"].locations.append("Monstro Mouth High Platform Across from Boat Chest")
|
||||
regions["Monstro"].locations.append("Monstro Mouth High Platform Boat Side Chest")
|
||||
regions["Monstro"].locations.append("Monstro Mouth High Platform Near Teeth Chest")
|
||||
regions["Monstro"].locations.append("Monstro Mouth Near Ship Chest")
|
||||
regions["Neverland"].locations.append("Neverland Cabin Chest")
|
||||
regions["Neverland"].locations.append("Neverland Captain's Cabin Chest")
|
||||
#regions["Neverland"].locations.append("Neverland Clock Tower 01:00 Door")
|
||||
#regions["Neverland"].locations.append("Neverland Clock Tower 02:00 Door")
|
||||
#regions["Neverland"].locations.append("Neverland Clock Tower 03:00 Door")
|
||||
#regions["Neverland"].locations.append("Neverland Clock Tower 04:00 Door")
|
||||
#regions["Neverland"].locations.append("Neverland Clock Tower 05:00 Door")
|
||||
#regions["Neverland"].locations.append("Neverland Clock Tower 06:00 Door")
|
||||
#regions["Neverland"].locations.append("Neverland Clock Tower 07:00 Door")
|
||||
#regions["Neverland"].locations.append("Neverland Clock Tower 08:00 Door")
|
||||
#regions["Neverland"].locations.append("Neverland Clock Tower 09:00 Door")
|
||||
#regions["Neverland"].locations.append("Neverland Clock Tower 10:00 Door")
|
||||
#regions["Neverland"].locations.append("Neverland Clock Tower 11:00 Door")
|
||||
#regions["Neverland"].locations.append("Neverland Clock Tower 12:00 Door")
|
||||
regions["Neverland"].locations.append("Neverland Clock Tower Chest")
|
||||
regions["Neverland"].locations.append("Neverland Defeat Anti Sora Raven's Claw Event")
|
||||
regions["Neverland"].locations.append("Neverland Defeat Captain Hook Ars Arcanum Event")
|
||||
regions["Neverland"].locations.append("Neverland Defeat Hook Ansem's Report 9")
|
||||
regions["Neverland"].locations.append("Neverland Encounter Hook Cure Event")
|
||||
regions["Neverland"].locations.append("Neverland Galley Chest")
|
||||
regions["Neverland"].locations.append("Neverland Hold Aero Chest")
|
||||
regions["Neverland"].locations.append("Neverland Hold Flight 1st Chest")
|
||||
regions["Neverland"].locations.append("Neverland Hold Flight 2nd Chest")
|
||||
regions["Neverland"].locations.append("Neverland Hold Yellow Trinity Green Chest")
|
||||
regions["Neverland"].locations.append("Neverland Hold Yellow Trinity Left Blue Chest")
|
||||
regions["Neverland"].locations.append("Neverland Hold Yellow Trinity Right Blue Chest")
|
||||
regions["Neverland"].locations.append("Neverland Pirate Ship Crows Nest Chest")
|
||||
regions["Neverland"].locations.append("Neverland Pirate Ship Deck White Trinity Chest")
|
||||
regions["Neverland"].locations.append("Neverland Seal Keyhole Fairy Harp Event")
|
||||
regions["Neverland"].locations.append("Neverland Seal Keyhole Glide Event")
|
||||
regions["Neverland"].locations.append("Neverland Seal Keyhole Tinker Bell Event")
|
||||
regions["Olympus Coliseum"].locations.append("Olympus Coliseum Clear Phil's Training Thunder Event")
|
||||
regions["Olympus Coliseum"].locations.append("Olympus Coliseum Cloud Sonic Blade Event")
|
||||
regions["Olympus Coliseum"].locations.append("Olympus Coliseum Coliseum Gates Blizzaga Chest")
|
||||
regions["Olympus Coliseum"].locations.append("Olympus Coliseum Coliseum Gates Blizzara Chest")
|
||||
regions["Olympus Coliseum"].locations.append("Olympus Coliseum Coliseum Gates Entry Pass Event")
|
||||
regions["Olympus Coliseum"].locations.append("Olympus Coliseum Coliseum Gates Green Trinity")
|
||||
regions["Olympus Coliseum"].locations.append("Olympus Coliseum Coliseum Gates Hero's License Event")
|
||||
regions["Olympus Coliseum"].locations.append("Olympus Coliseum Coliseum Gates Left Behind Columns Chest")
|
||||
regions["Olympus Coliseum"].locations.append("Olympus Coliseum Coliseum Gates Left Blue Trinity Chest")
|
||||
regions["Olympus Coliseum"].locations.append("Olympus Coliseum Coliseum Gates Right Blue Trinity Chest")
|
||||
regions["Olympus Coliseum"].locations.append("Olympus Coliseum Coliseum Gates White Trinity Chest")
|
||||
regions["Olympus Coliseum"].locations.append("Olympus Coliseum Defeat Cerberus Inferno Band Event")
|
||||
regions["Traverse Town"].locations.append("Traverse Town 1st District Accessory Shop Roof Chest")
|
||||
#regions["Traverse Town"].locations.append("Traverse Town 1st District Aerith Gift")
|
||||
regions["Traverse Town"].locations.append("Traverse Town 1st District Blue Trinity Balcony Chest")
|
||||
regions["Traverse Town"].locations.append("Traverse Town 1st District Candle Puzzle Chest")
|
||||
#regions["Traverse Town"].locations.append("Traverse Town 1st District Leon Gift")
|
||||
regions["Traverse Town"].locations.append("Traverse Town 1st District Safe Postcard")
|
||||
regions["Traverse Town"].locations.append("Traverse Town 1st District Speak with Cid Event")
|
||||
regions["Traverse Town"].locations.append("Traverse Town 2nd District Boots and Shoes Awning Chest")
|
||||
regions["Traverse Town"].locations.append("Traverse Town 2nd District Gizmo Shop Facade Chest")
|
||||
regions["Traverse Town"].locations.append("Traverse Town 2nd District Rooftop Chest")
|
||||
regions["Traverse Town"].locations.append("Traverse Town 3rd District Balcony Postcard")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Accessory Shop Chest")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Alleyway Balcony Chest")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Alleyway Behind Crates Chest")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Alleyway Blue Room Awning Chest")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Alleyway Corner Chest")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Defeat Guard Armor Blue Trinity Event")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Defeat Guard Armor Brave Warrior Event")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Defeat Guard Armor Dodge Roll Event")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Defeat Guard Armor Fire Event")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Defeat Opposite Armor Aero Event")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Geppetto's House Chest")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Geppetto's House Geppetto All Summons Reward")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Geppetto's House Geppetto Reward 1")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Geppetto's House Geppetto Reward 2")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Geppetto's House Geppetto Reward 3")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Geppetto's House Geppetto Reward 4")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Geppetto's House Geppetto Reward 5")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Geppetto's House Postcard")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Geppetto's House Talk to Pinocchio")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Gizmo Shop Postcard 1")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Gizmo Shop Postcard 2")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Green Room Clock Puzzle Chest")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Green Room Table Chest")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Item Shop Postcard")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Item Workshop Left Chest")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Item Workshop Postcard")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Item Workshop Right Chest")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Kairi Secret Waterway Oathkeeper Event")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Leon Secret Waterway Earthshine Event")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Magician's Study Obtained All Arts Items")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Magician's Study Obtained All LV1 Magic")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Magician's Study Obtained All LV3 Magic")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Mail Postcard 01 Event")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Mail Postcard 02 Event")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Mail Postcard 03 Event")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Mail Postcard 04 Event")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Mail Postcard 05 Event")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Mail Postcard 06 Event")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Mail Postcard 07 Event")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Mail Postcard 08 Event")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Mail Postcard 09 Event")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Mail Postcard 10 Event")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Mystical House Glide Chest")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Mystical House Yellow Trinity Chest")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Piano Room Return 10 Puppies")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Piano Room Return 20 Puppies")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Piano Room Return 30 Puppies")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Piano Room Return 40 Puppies")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Piano Room Return 50 Puppies Reward 1")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Piano Room Return 50 Puppies Reward 2")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Piano Room Return 60 Puppies")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Piano Room Return 70 Puppies")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Piano Room Return 80 Puppies")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Piano Room Return 90 Puppies")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Piano Room Return 99 Puppies Reward 1")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Piano Room Return 99 Puppies Reward 2")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Red Room Chest")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Secret Waterway Near Stairs Chest")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Secret Waterway White Trinity Chest")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Synth Cloth")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Synth Fish")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Synth Log")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Synth Mushroom")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Synth Rope")
|
||||
regions["Traverse Town"].locations.append("Traverse Town Synth Seagull Egg")
|
||||
regions["Wonderland"].locations.append("Wonderland Bizarre Room Green Trinity Chest")
|
||||
regions["Wonderland"].locations.append("Wonderland Bizarre Room Lamp Chest")
|
||||
regions["Wonderland"].locations.append("Wonderland Bizarre Room Navi-G Piece Event")
|
||||
regions["Wonderland"].locations.append("Wonderland Bizarre Room Read Book")
|
||||
regions["Wonderland"].locations.append("Wonderland Defeat Trickmaster Blizzard Event")
|
||||
regions["Wonderland"].locations.append("Wonderland Defeat Trickmaster Ifrit's Horn Event")
|
||||
regions["Wonderland"].locations.append("Wonderland Lotus Forest Corner Chest")
|
||||
regions["Wonderland"].locations.append("Wonderland Lotus Forest Glide Chest")
|
||||
regions["Wonderland"].locations.append("Wonderland Lotus Forest Nut Chest")
|
||||
regions["Wonderland"].locations.append("Wonderland Lotus Forest Through the Painting Thunder Plant Chest")
|
||||
regions["Wonderland"].locations.append("Wonderland Lotus Forest Through the Painting White Trinity Chest")
|
||||
regions["Wonderland"].locations.append("Wonderland Lotus Forest Thunder Plant Chest")
|
||||
regions["Wonderland"].locations.append("Wonderland Queen's Castle Hedge Left Red Chest")
|
||||
regions["Wonderland"].locations.append("Wonderland Queen's Castle Hedge Right Blue Chest")
|
||||
regions["Wonderland"].locations.append("Wonderland Queen's Castle Hedge Right Red Chest")
|
||||
regions["Wonderland"].locations.append("Wonderland Rabbit Hole Defeat Heartless 1 Chest")
|
||||
regions["Wonderland"].locations.append("Wonderland Rabbit Hole Defeat Heartless 2 Chest")
|
||||
regions["Wonderland"].locations.append("Wonderland Rabbit Hole Defeat Heartless 3 Chest")
|
||||
regions["Wonderland"].locations.append("Wonderland Rabbit Hole Green Trinity Chest")
|
||||
regions["Wonderland"].locations.append("Wonderland Tea Party Garden Above Lotus Forest Entrance 1st Chest")
|
||||
regions["Wonderland"].locations.append("Wonderland Tea Party Garden Above Lotus Forest Entrance 2nd Chest")
|
||||
regions["Wonderland"].locations.append("Wonderland Tea Party Garden Across From Bizarre Room Entrance Chest")
|
||||
regions["Wonderland"].locations.append("Wonderland Tea Party Garden Bear and Clock Puzzle Chest")
|
||||
if options.hundred_acre_wood:
|
||||
regions["100 Acre Wood"].locations.append("100 Acre Wood Meadow Inside Log Chest")
|
||||
regions["100 Acre Wood"].locations.append("100 Acre Wood Bouncing Spot Left Cliff Chest")
|
||||
regions["100 Acre Wood"].locations.append("100 Acre Wood Bouncing Spot Right Tree Alcove Chest")
|
||||
regions["100 Acre Wood"].locations.append("100 Acre Wood Bouncing Spot Under Giant Pot Chest")
|
||||
regions["100 Acre Wood"].locations.append("100 Acre Wood Bouncing Spot Turn in Rare Nut 1")
|
||||
regions["100 Acre Wood"].locations.append("100 Acre Wood Bouncing Spot Turn in Rare Nut 2")
|
||||
regions["100 Acre Wood"].locations.append("100 Acre Wood Bouncing Spot Turn in Rare Nut 3")
|
||||
regions["100 Acre Wood"].locations.append("100 Acre Wood Bouncing Spot Turn in Rare Nut 4")
|
||||
regions["100 Acre Wood"].locations.append("100 Acre Wood Bouncing Spot Turn in Rare Nut 5")
|
||||
regions["100 Acre Wood"].locations.append("100 Acre Wood Pooh's House Owl Cheer")
|
||||
regions["100 Acre Wood"].locations.append("100 Acre Wood Convert Torn Page 1")
|
||||
regions["100 Acre Wood"].locations.append("100 Acre Wood Convert Torn Page 2")
|
||||
regions["100 Acre Wood"].locations.append("100 Acre Wood Convert Torn Page 3")
|
||||
regions["100 Acre Wood"].locations.append("100 Acre Wood Convert Torn Page 4")
|
||||
regions["100 Acre Wood"].locations.append("100 Acre Wood Convert Torn Page 5")
|
||||
regions["100 Acre Wood"].locations.append("100 Acre Wood Pooh's House Start Fire")
|
||||
regions["100 Acre Wood"].locations.append("100 Acre Wood Pooh's Room Cabinet")
|
||||
regions["100 Acre Wood"].locations.append("100 Acre Wood Pooh's Room Chimney")
|
||||
regions["100 Acre Wood"].locations.append("100 Acre Wood Bouncing Spot Break Log")
|
||||
regions["100 Acre Wood"].locations.append("100 Acre Wood Bouncing Spot Fall Through Top of Tree Next to Pooh")
|
||||
if options.atlantica:
|
||||
regions["Atlantica"].locations.append("Atlantica Sunken Ship In Flipped Boat Chest")
|
||||
regions["Atlantica"].locations.append("Atlantica Sunken Ship Seabed Chest")
|
||||
regions["Atlantica"].locations.append("Atlantica Sunken Ship Inside Ship Chest")
|
||||
regions["Atlantica"].locations.append("Atlantica Ariel's Grotto High Chest")
|
||||
regions["Atlantica"].locations.append("Atlantica Ariel's Grotto Middle Chest")
|
||||
regions["Atlantica"].locations.append("Atlantica Ariel's Grotto Low Chest")
|
||||
regions["Atlantica"].locations.append("Atlantica Ursula's Lair Use Fire on Urchin Chest")
|
||||
regions["Atlantica"].locations.append("Atlantica Undersea Gorge Jammed by Ariel's Grotto Chest")
|
||||
regions["Atlantica"].locations.append("Atlantica Triton's Palace White Trinity Chest")
|
||||
regions["Atlantica"].locations.append("Atlantica Defeat Ursula I Mermaid Kick Event")
|
||||
regions["Atlantica"].locations.append("Atlantica Defeat Ursula II Thunder Event")
|
||||
regions["Atlantica"].locations.append("Atlantica Seal Keyhole Crabclaw Event")
|
||||
regions["Atlantica"].locations.append("Atlantica Undersea Gorge Blizzard Clam")
|
||||
regions["Atlantica"].locations.append("Atlantica Undersea Gorge Ocean Floor Clam")
|
||||
regions["Atlantica"].locations.append("Atlantica Undersea Valley Higher Cave Clam")
|
||||
regions["Atlantica"].locations.append("Atlantica Undersea Valley Lower Cave Clam")
|
||||
regions["Atlantica"].locations.append("Atlantica Undersea Valley Fire Clam")
|
||||
regions["Atlantica"].locations.append("Atlantica Undersea Valley Wall Clam")
|
||||
regions["Atlantica"].locations.append("Atlantica Undersea Valley Pillar Clam")
|
||||
regions["Atlantica"].locations.append("Atlantica Undersea Valley Ocean Floor Clam")
|
||||
regions["Atlantica"].locations.append("Atlantica Triton's Palace Thunder Clam")
|
||||
regions["Atlantica"].locations.append("Atlantica Triton's Palace Wall Right Clam")
|
||||
regions["Atlantica"].locations.append("Atlantica Triton's Palace Near Path Clam")
|
||||
regions["Atlantica"].locations.append("Atlantica Triton's Palace Wall Left Clam")
|
||||
regions["Atlantica"].locations.append("Atlantica Cavern Nook Clam")
|
||||
regions["Atlantica"].locations.append("Atlantica Below Deck Clam")
|
||||
regions["Atlantica"].locations.append("Atlantica Undersea Garden Clam")
|
||||
regions["Atlantica"].locations.append("Atlantica Undersea Cave Clam")
|
||||
regions["Atlantica"].locations.append("Atlantica Sunken Ship Crystal Trident Event")
|
||||
regions["Atlantica"].locations.append("Atlantica Defeat Ursula II Ansem's Report 3")
|
||||
if options.cups:
|
||||
regions["Olympus Coliseum"].locations.append("Complete Phil Cup")
|
||||
regions["Olympus Coliseum"].locations.append("Complete Phil Cup Solo")
|
||||
regions["Olympus Coliseum"].locations.append("Complete Phil Cup Time Trial")
|
||||
regions["Olympus Coliseum"].locations.append("Complete Pegasus Cup")
|
||||
regions["Olympus Coliseum"].locations.append("Complete Pegasus Cup Solo")
|
||||
regions["Olympus Coliseum"].locations.append("Complete Pegasus Cup Time Trial")
|
||||
regions["Olympus Coliseum"].locations.append("Complete Hercules Cup")
|
||||
regions["Olympus Coliseum"].locations.append("Complete Hercules Cup Solo")
|
||||
regions["Olympus Coliseum"].locations.append("Complete Hercules Cup Time Trial")
|
||||
regions["Olympus Coliseum"].locations.append("Complete Hades Cup")
|
||||
regions["Olympus Coliseum"].locations.append("Complete Hades Cup Solo")
|
||||
regions["Olympus Coliseum"].locations.append("Complete Hades Cup Time Trial")
|
||||
regions["Olympus Coliseum"].locations.append("Hades Cup Defeat Cloud and Leon Event")
|
||||
regions["Olympus Coliseum"].locations.append("Hades Cup Defeat Yuffie Event")
|
||||
regions["Olympus Coliseum"].locations.append("Hades Cup Defeat Cerberus Event")
|
||||
regions["Olympus Coliseum"].locations.append("Hades Cup Defeat Behemoth Event")
|
||||
regions["Olympus Coliseum"].locations.append("Hades Cup Defeat Hades Event")
|
||||
regions["Olympus Coliseum"].locations.append("Hercules Cup Defeat Cloud Event")
|
||||
regions["Olympus Coliseum"].locations.append("Hercules Cup Yellow Trinity Event")
|
||||
regions["Olympus Coliseum"].locations.append("Olympus Coliseum Defeat Hades Ansem's Report 8")
|
||||
regions["Olympus Coliseum"].locations.append("Olympus Coliseum Olympia Chest")
|
||||
regions["Olympus Coliseum"].locations.append("Olympus Coliseum Defeat Ice Titan Diamond Dust Event")
|
||||
regions["Olympus Coliseum"].locations.append("Olympus Coliseum Gates Purple Jar After Defeating Hades")
|
||||
if options.super_bosses:
|
||||
regions["Neverland"].locations.append("Neverland Defeat Phantom Stop Event")
|
||||
regions["Agrabah"].locations.append("Agrabah Defeat Kurt Zisa Zantetsuken Event")
|
||||
regions["Agrabah"].locations.append("Agrabah Defeat Kurt Zisa Ansem's Report 11")
|
||||
if options.super_bosses or options.goal.current_key == "sephiroth":
|
||||
regions["Olympus Coliseum"].locations.append("Olympus Coliseum Defeat Sephiroth Ansem's Report 12")
|
||||
regions["Olympus Coliseum"].locations.append("Olympus Coliseum Defeat Sephiroth One-Winged Angel Event")
|
||||
if options.super_bosses or options.goal.current_key == "unknown":
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Defeat Unknown Ansem's Report 13")
|
||||
regions["Hollow Bastion"].locations.append("Hollow Bastion Defeat Unknown EXP Necklace Event")
|
||||
for i in range(options.level_checks):
|
||||
regions["Levels"].locations.append("Level " + str(i+1).rjust(3, '0'))
|
||||
if options.goal.current_key == "final_ansem":
|
||||
regions["End of the World"].locations.append("Final Ansem")
|
||||
|
||||
# Set up the regions correctly.
|
||||
for name, data in regions.items():
|
||||
multiworld.regions.append(create_region(multiworld, player, name, data))
|
||||
|
||||
multiworld.get_entrance("Awakening", player).connect(multiworld.get_region("Awakening", player))
|
||||
multiworld.get_entrance("Destiny Islands", player).connect(multiworld.get_region("Destiny Islands", player))
|
||||
multiworld.get_entrance("Traverse Town", player).connect(multiworld.get_region("Traverse Town", player))
|
||||
multiworld.get_entrance("Wonderland", player).connect(multiworld.get_region("Wonderland", player))
|
||||
multiworld.get_entrance("Olympus Coliseum", player).connect(multiworld.get_region("Olympus Coliseum", player))
|
||||
multiworld.get_entrance("Deep Jungle", player).connect(multiworld.get_region("Deep Jungle", player))
|
||||
multiworld.get_entrance("Agrabah", player).connect(multiworld.get_region("Agrabah", player))
|
||||
multiworld.get_entrance("Monstro", player).connect(multiworld.get_region("Monstro", player))
|
||||
multiworld.get_entrance("Atlantica", player).connect(multiworld.get_region("Atlantica", player))
|
||||
multiworld.get_entrance("Halloween Town", player).connect(multiworld.get_region("Halloween Town", player))
|
||||
multiworld.get_entrance("Neverland", player).connect(multiworld.get_region("Neverland", player))
|
||||
multiworld.get_entrance("Hollow Bastion", player).connect(multiworld.get_region("Hollow Bastion", player))
|
||||
multiworld.get_entrance("End of the World", player).connect(multiworld.get_region("End of the World", player))
|
||||
multiworld.get_entrance("100 Acre Wood", player).connect(multiworld.get_region("100 Acre Wood", player))
|
||||
multiworld.get_entrance("World Map", player).connect(multiworld.get_region("World Map", player))
|
||||
multiworld.get_entrance("Levels", player).connect(multiworld.get_region("Levels", player))
|
||||
|
||||
def create_region(multiworld: MultiWorld, player: int, name: str, data: KH1RegionData):
|
||||
region = Region(name, player, multiworld)
|
||||
if data.locations:
|
||||
for loc_name in data.locations:
|
||||
loc_data = location_table.get(loc_name)
|
||||
location = KH1Location(player, loc_name, loc_data.code if loc_data else None, region)
|
||||
region.locations.append(location)
|
||||
|
||||
if data.region_exits:
|
||||
for exit in data.region_exits:
|
||||
entrance = Entrance(player, exit, region)
|
||||
region.exits.append(entrance)
|
||||
|
||||
return region
|
||||
1948
worlds/kh1/Rules.py
Normal file
1948
worlds/kh1/Rules.py
Normal file
File diff suppressed because it is too large
Load Diff
282
worlds/kh1/__init__.py
Normal file
282
worlds/kh1/__init__.py
Normal file
@@ -0,0 +1,282 @@
|
||||
import logging
|
||||
from typing import List
|
||||
|
||||
from BaseClasses import Tutorial
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
from .Items import KH1Item, KH1ItemData, event_item_table, get_items_by_category, item_table, item_name_groups
|
||||
from .Locations import KH1Location, location_table, get_locations_by_category, location_name_groups
|
||||
from .Options import KH1Options, kh1_option_groups
|
||||
from .Regions import create_regions
|
||||
from .Rules import set_rules
|
||||
from .Presets import kh1_option_presets
|
||||
from worlds.LauncherComponents import Component, components, Type, launch_subprocess
|
||||
|
||||
|
||||
def launch_client():
|
||||
from .Client import launch
|
||||
launch_subprocess(launch, name="KH1 Client")
|
||||
|
||||
|
||||
components.append(Component("KH1 Client", "KH1Client", func=launch_client, component_type=Type.CLIENT))
|
||||
|
||||
|
||||
class KH1Web(WebWorld):
|
||||
theme = "ocean"
|
||||
tutorials = [Tutorial(
|
||||
"Multiworld Setup Guide",
|
||||
"A guide to setting up the Kingdom Hearts Randomizer software on your computer."
|
||||
"This guide covers single-player, multiworld, and related software.",
|
||||
"English",
|
||||
"kh1_en.md",
|
||||
"kh1/en",
|
||||
["Gicu"]
|
||||
)]
|
||||
option_groups = kh1_option_groups
|
||||
options_presets = kh1_option_presets
|
||||
|
||||
|
||||
class KH1World(World):
|
||||
"""
|
||||
Kingdom Hearts is an action RPG following Sora on his journey
|
||||
through many worlds to find Riku and Kairi.
|
||||
"""
|
||||
game = "Kingdom Hearts"
|
||||
options_dataclass = KH1Options
|
||||
options: KH1Options
|
||||
topology_present = True
|
||||
web = KH1Web()
|
||||
|
||||
item_name_to_id = {name: data.code for name, data in item_table.items()}
|
||||
location_name_to_id = {name: data.code for name, data in location_table.items()}
|
||||
item_name_groups = item_name_groups
|
||||
location_name_groups = location_name_groups
|
||||
fillers = {}
|
||||
fillers.update(get_items_by_category("Item"))
|
||||
fillers.update(get_items_by_category("Camping"))
|
||||
fillers.update(get_items_by_category("Stat Ups"))
|
||||
|
||||
def create_items(self):
|
||||
self.place_predetermined_items()
|
||||
# Handle starting worlds
|
||||
starting_worlds = []
|
||||
if self.options.starting_worlds > 0:
|
||||
possible_starting_worlds = ["Wonderland", "Olympus Coliseum", "Deep Jungle", "Agrabah", "Monstro", "Halloween Town", "Neverland", "Hollow Bastion"]
|
||||
if self.options.atlantica:
|
||||
possible_starting_worlds.append("Atlantica")
|
||||
if self.options.end_of_the_world_unlock == "item":
|
||||
possible_starting_worlds.append("End of the World")
|
||||
starting_worlds = self.random.sample(possible_starting_worlds, min(self.options.starting_worlds.value, len(possible_starting_worlds)))
|
||||
for starting_world in starting_worlds:
|
||||
self.multiworld.push_precollected(self.create_item(starting_world))
|
||||
|
||||
item_pool: List[KH1Item] = []
|
||||
possible_level_up_item_pool = []
|
||||
level_up_item_pool = []
|
||||
|
||||
# Calculate Level Up Items
|
||||
# Fill pool with mandatory items
|
||||
for _ in range(self.options.item_slot_increase):
|
||||
level_up_item_pool.append("Item Slot Increase")
|
||||
for _ in range(self.options.accessory_slot_increase):
|
||||
level_up_item_pool.append("Accessory Slot Increase")
|
||||
|
||||
# Create other pool
|
||||
for _ in range(self.options.strength_increase):
|
||||
possible_level_up_item_pool.append("Strength Increase")
|
||||
for _ in range(self.options.defense_increase):
|
||||
possible_level_up_item_pool.append("Defense Increase")
|
||||
for _ in range(self.options.hp_increase):
|
||||
possible_level_up_item_pool.append("Max HP Increase")
|
||||
for _ in range(self.options.mp_increase):
|
||||
possible_level_up_item_pool.append("Max MP Increase")
|
||||
for _ in range(self.options.ap_increase):
|
||||
possible_level_up_item_pool.append("Max AP Increase")
|
||||
|
||||
# Fill remaining pool with items from other pool
|
||||
self.random.shuffle(possible_level_up_item_pool)
|
||||
level_up_item_pool = level_up_item_pool + possible_level_up_item_pool[:(100 - len(level_up_item_pool))]
|
||||
|
||||
level_up_locations = list(get_locations_by_category("Levels").keys())
|
||||
self.random.shuffle(level_up_item_pool)
|
||||
current_level_for_placing_stats = self.options.force_stats_on_levels.value
|
||||
while len(level_up_item_pool) > 0 and current_level_for_placing_stats <= self.options.level_checks:
|
||||
self.get_location(level_up_locations[current_level_for_placing_stats - 1]).place_locked_item(self.create_item(level_up_item_pool.pop()))
|
||||
current_level_for_placing_stats += 1
|
||||
|
||||
# Calculate prefilled locations and items
|
||||
prefilled_items = []
|
||||
if self.options.vanilla_emblem_pieces:
|
||||
prefilled_items = prefilled_items + ["Emblem Piece (Flame)", "Emblem Piece (Chest)", "Emblem Piece (Fountain)", "Emblem Piece (Statue)"]
|
||||
|
||||
total_locations = len(self.multiworld.get_unfilled_locations(self.player))
|
||||
|
||||
non_filler_item_categories = ["Key", "Magic", "Worlds", "Trinities", "Cups", "Summons", "Abilities", "Shared Abilities", "Keyblades", "Accessory", "Weapons", "Puppies"]
|
||||
if self.options.hundred_acre_wood:
|
||||
non_filler_item_categories.append("Torn Pages")
|
||||
for name, data in item_table.items():
|
||||
quantity = data.max_quantity
|
||||
if data.category not in non_filler_item_categories:
|
||||
continue
|
||||
if name in starting_worlds:
|
||||
continue
|
||||
if data.category == "Puppies":
|
||||
if self.options.puppies == "triplets" and "-" in name:
|
||||
item_pool += [self.create_item(name) for _ in range(quantity)]
|
||||
if self.options.puppies == "individual" and "Puppy" in name:
|
||||
item_pool += [self.create_item(name) for _ in range(0, quantity)]
|
||||
if self.options.puppies == "full" and name == "All Puppies":
|
||||
item_pool += [self.create_item(name) for _ in range(0, quantity)]
|
||||
elif name == "Atlantica":
|
||||
if self.options.atlantica:
|
||||
item_pool += [self.create_item(name) for _ in range(0, quantity)]
|
||||
elif name == "Mermaid Kick":
|
||||
if self.options.atlantica:
|
||||
if self.options.extra_shared_abilities:
|
||||
item_pool += [self.create_item(name) for _ in range(0, 2)]
|
||||
else:
|
||||
item_pool += [self.create_item(name) for _ in range(0, quantity)]
|
||||
elif name == "Crystal Trident":
|
||||
if self.options.atlantica:
|
||||
item_pool += [self.create_item(name) for _ in range(0, quantity)]
|
||||
elif name == "High Jump":
|
||||
if self.options.extra_shared_abilities:
|
||||
item_pool += [self.create_item(name) for _ in range(0, 3)]
|
||||
else:
|
||||
item_pool += [self.create_item(name) for _ in range(0, quantity)]
|
||||
elif name == "Progressive Glide":
|
||||
if self.options.extra_shared_abilities:
|
||||
item_pool += [self.create_item(name) for _ in range(0, 4)]
|
||||
else:
|
||||
item_pool += [self.create_item(name) for _ in range(0, quantity)]
|
||||
elif name == "End of the World":
|
||||
if self.options.end_of_the_world_unlock.current_key == "item":
|
||||
item_pool += [self.create_item(name) for _ in range(0, quantity)]
|
||||
elif name == "EXP Zero":
|
||||
if self.options.exp_zero_in_pool:
|
||||
item_pool += [self.create_item(name) for _ in range(0, quantity)]
|
||||
elif name not in prefilled_items:
|
||||
item_pool += [self.create_item(name) for _ in range(0, quantity)]
|
||||
|
||||
for i in range(self.determine_reports_in_pool()):
|
||||
item_pool += [self.create_item("Ansem's Report " + str(i+1))]
|
||||
|
||||
while len(item_pool) < total_locations and len(level_up_item_pool) > 0:
|
||||
item_pool += [self.create_item(level_up_item_pool.pop())]
|
||||
|
||||
# Fill any empty locations with filler items.
|
||||
while len(item_pool) < total_locations:
|
||||
item_pool.append(self.create_item(self.get_filler_item_name()))
|
||||
|
||||
self.multiworld.itempool += item_pool
|
||||
|
||||
def place_predetermined_items(self) -> None:
|
||||
goal_dict = {
|
||||
"sephiroth": "Olympus Coliseum Defeat Sephiroth Ansem's Report 12",
|
||||
"unknown": "Hollow Bastion Defeat Unknown Ansem's Report 13",
|
||||
"postcards": "Traverse Town Mail Postcard 10 Event",
|
||||
"final_ansem": "Final Ansem",
|
||||
"puppies": "Traverse Town Piano Room Return 99 Puppies Reward 2",
|
||||
"final_rest": "End of the World Final Rest Chest"
|
||||
}
|
||||
self.get_location(goal_dict[self.options.goal.current_key]).place_locked_item(self.create_item("Victory"))
|
||||
if self.options.vanilla_emblem_pieces:
|
||||
self.get_location("Hollow Bastion Entrance Hall Emblem Piece (Flame)").place_locked_item(self.create_item("Emblem Piece (Flame)"))
|
||||
self.get_location("Hollow Bastion Entrance Hall Emblem Piece (Statue)").place_locked_item(self.create_item("Emblem Piece (Statue)"))
|
||||
self.get_location("Hollow Bastion Entrance Hall Emblem Piece (Fountain)").place_locked_item(self.create_item("Emblem Piece (Fountain)"))
|
||||
self.get_location("Hollow Bastion Entrance Hall Emblem Piece (Chest)").place_locked_item(self.create_item("Emblem Piece (Chest)"))
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
weights = [data.weight for data in self.fillers.values()]
|
||||
return self.random.choices([filler for filler in self.fillers.keys()], weights)[0]
|
||||
|
||||
def fill_slot_data(self) -> dict:
|
||||
slot_data = {"xpmult": int(self.options.exp_multiplier)/16,
|
||||
"required_reports_eotw": self.determine_reports_required_to_open_end_of_the_world(),
|
||||
"required_reports_door": self.determine_reports_required_to_open_final_rest_door(),
|
||||
"door": self.options.final_rest_door.current_key,
|
||||
"seed": self.multiworld.seed_name,
|
||||
"advanced_logic": bool(self.options.advanced_logic),
|
||||
"hundred_acre_wood": bool(self.options.hundred_acre_wood),
|
||||
"atlantica": bool(self.options.atlantica),
|
||||
"goal": str(self.options.goal.current_key)}
|
||||
if self.options.randomize_keyblade_stats:
|
||||
min_str_bonus = min(self.options.keyblade_min_str.value, self.options.keyblade_max_str.value)
|
||||
max_str_bonus = max(self.options.keyblade_min_str.value, self.options.keyblade_max_str.value)
|
||||
self.options.keyblade_min_str.value = min_str_bonus
|
||||
self.options.keyblade_max_str.value = max_str_bonus
|
||||
min_mp_bonus = min(self.options.keyblade_min_mp.value, self.options.keyblade_max_mp.value)
|
||||
max_mp_bonus = max(self.options.keyblade_min_mp.value, self.options.keyblade_max_mp.value)
|
||||
self.options.keyblade_min_mp.value = min_mp_bonus
|
||||
self.options.keyblade_max_mp.value = max_mp_bonus
|
||||
slot_data["keyblade_stats"] = ""
|
||||
for i in range(22):
|
||||
if i < 4 and self.options.bad_starting_weapons:
|
||||
slot_data["keyblade_stats"] = slot_data["keyblade_stats"] + "1,0,"
|
||||
else:
|
||||
str_bonus = int(self.random.randint(min_str_bonus, max_str_bonus))
|
||||
mp_bonus = int(self.random.randint(min_mp_bonus, max_mp_bonus))
|
||||
slot_data["keyblade_stats"] = slot_data["keyblade_stats"] + str(str_bonus) + "," + str(mp_bonus) + ","
|
||||
slot_data["keyblade_stats"] = slot_data["keyblade_stats"][:-1]
|
||||
if self.options.donald_death_link:
|
||||
slot_data["donalddl"] = ""
|
||||
if self.options.goofy_death_link:
|
||||
slot_data["goofydl"] = ""
|
||||
if self.options.keyblades_unlock_chests:
|
||||
slot_data["chestslocked"] = ""
|
||||
else:
|
||||
slot_data["chestsunlocked"] = ""
|
||||
if self.options.interact_in_battle:
|
||||
slot_data["interactinbattle"] = ""
|
||||
return slot_data
|
||||
|
||||
def create_item(self, name: str) -> KH1Item:
|
||||
data = item_table[name]
|
||||
return KH1Item(name, data.classification, data.code, self.player)
|
||||
|
||||
def create_event(self, name: str) -> KH1Item:
|
||||
data = event_item_table[name]
|
||||
return KH1Item(name, data.classification, data.code, self.player)
|
||||
|
||||
def set_rules(self):
|
||||
set_rules(self)
|
||||
|
||||
def create_regions(self):
|
||||
create_regions(self.multiworld, self.player, self.options)
|
||||
|
||||
def generate_early(self):
|
||||
value_names = ["Reports to Open End of the World", "Reports to Open Final Rest Door", "Reports in Pool"]
|
||||
initial_report_settings = [self.options.required_reports_eotw.value, self.options.required_reports_door.value, self.options.reports_in_pool.value]
|
||||
self.change_numbers_of_reports_to_consider()
|
||||
new_report_settings = [self.options.required_reports_eotw.value, self.options.required_reports_door.value, self.options.reports_in_pool.value]
|
||||
for i in range(3):
|
||||
if initial_report_settings[i] != new_report_settings[i]:
|
||||
logging.info(f"{self.player_name}'s value {initial_report_settings[i]} for \"{value_names[i]}\" was invalid\n"
|
||||
f"Setting \"{value_names[i]}\" value to {new_report_settings[i]}")
|
||||
|
||||
def change_numbers_of_reports_to_consider(self) -> None:
|
||||
if self.options.end_of_the_world_unlock == "reports" and self.options.final_rest_door == "reports":
|
||||
self.options.required_reports_eotw.value, self.options.required_reports_door.value, self.options.reports_in_pool.value = sorted(
|
||||
[self.options.required_reports_eotw.value, self.options.required_reports_door.value, self.options.reports_in_pool.value])
|
||||
|
||||
elif self.options.end_of_the_world_unlock == "reports":
|
||||
self.options.required_reports_eotw.value, self.options.reports_in_pool.value = sorted(
|
||||
[self.options.required_reports_eotw.value, self.options.reports_in_pool.value])
|
||||
|
||||
elif self.options.final_rest_door == "reports":
|
||||
self.options.required_reports_door.value, self.options.reports_in_pool.value = sorted(
|
||||
[self.options.required_reports_door.value, self.options.reports_in_pool.value])
|
||||
|
||||
def determine_reports_in_pool(self) -> int:
|
||||
if self.options.end_of_the_world_unlock == "reports" or self.options.final_rest_door == "reports":
|
||||
return self.options.reports_in_pool.value
|
||||
return 0
|
||||
|
||||
def determine_reports_required_to_open_end_of_the_world(self) -> int:
|
||||
if self.options.end_of_the_world_unlock == "reports":
|
||||
return self.options.required_reports_eotw.value
|
||||
return 14
|
||||
|
||||
def determine_reports_required_to_open_final_rest_door(self) -> int:
|
||||
if self.options.final_rest_door == "reports":
|
||||
return self.options.required_reports_door.value
|
||||
return 14
|
||||
88
worlds/kh1/docs/en_Kingdom Hearts.md
Normal file
88
worlds/kh1/docs/en_Kingdom Hearts.md
Normal file
@@ -0,0 +1,88 @@
|
||||
# Kingdom Hearts (PC)
|
||||
|
||||
## Where is the options page?
|
||||
|
||||
The [player options page for this game](../player-options) contains most of the options you need to
|
||||
configure and export a config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
|
||||
The Kingdom Hearts AP Randomizer randomizes most rewards in the game and adds several items which are used to unlock worlds, Olympus Coliseum cups, and world progression.
|
||||
|
||||
Worlds can only be accessed by finding the corresponding item. For example, you need to find the `Monstro` item to enter Monstro.
|
||||
|
||||
The default goal is to enter End of the World and defeat Final Ansem.
|
||||
|
||||
## What items and locations get shuffled?
|
||||
|
||||
### Items
|
||||
|
||||
Any weapon, accessory, spell, trinity, summon, world, key item, stat up, consumable, or ability can be found in any location.
|
||||
|
||||
### Locations
|
||||
|
||||
Locations the player can find items include chests, event rewards, Atlantica clams, level up rewards, 101 Dalmatian rewards, and postcard rewards.
|
||||
|
||||
## Which items can be in another player's world?
|
||||
|
||||
Any of the items which can be shuffled may also be placed into another player's world. It is possible to choose to limit
|
||||
certain items to your own world.
|
||||
## When the player receives an item, what happens?
|
||||
|
||||
When the player receives an item, your client will display a message displaying the item you have obtained. You will also see a notification in the "LEVEL UP" box.
|
||||
|
||||
## What do I do if I encounter a bug with the game?
|
||||
|
||||
Please reach out to Gicu#7034 on Discord.
|
||||
|
||||
## How do I progress in a certain world?
|
||||
|
||||
### The evidence boxes aren't spawning in Wonderland.
|
||||
|
||||
Find `Footprints` in the multiworld.
|
||||
|
||||
### I can't enter any cups in Olympus Coliseum.
|
||||
|
||||
Firstly, find `Entry Pass` in the multiworld. Additionally, `Phil Cup`, `Pegasus Cup`, and `Hercules Cup` are all multiworld items. Finding all 3 grant you access to the Hades Cup and the Platinum Match. Clearing all cups lets you challenge Ice Titan.
|
||||
|
||||
### The slides aren't spawning in Deep Jungle.
|
||||
|
||||
Find `Slides` in the multiworld.
|
||||
|
||||
### I can't progress in Atlantica.
|
||||
Find `Crystal Trident` in the multiworld.
|
||||
|
||||
### I can't progress in Halloween Town.
|
||||
|
||||
Find `Forget-Me-Not` and `Jack-in-the-Box` in the multiworld.
|
||||
|
||||
### The Hollow Bastion Library is missing a book.
|
||||
|
||||
Find `Theon Vol. 6` in the multiworld.
|
||||
|
||||
## How do I enter the End of the World?
|
||||
|
||||
You can enter End of the World by obtaining a number of Ansem's Reports or by finding `End of the World` in the multiworld, depending on your options.
|
||||
|
||||
## Credits
|
||||
This is a collaborative effort from several individuals in the Kingdom Hearts community, but most of all, denhonator.
|
||||
|
||||
Denho's original KH rando laid the foundation for the work here and makes everything here possible, so thank you Denho for such a blast of a randomizer.
|
||||
|
||||
Other credits include:
|
||||
|
||||
Sonicshadowsilver2 for their work finding many memory addresses, working to identify and resolve bugs, and converting the code base to the latest EGS update.
|
||||
|
||||
Shananas and the rest of the OpenKH team for providing such an amazing tool for us to utilize on this project.
|
||||
|
||||
TopazTK for their work on the `Show Prompt` method and Krujo for their implementation of the method in AP.
|
||||
|
||||
JaredWeakStrike for helping clean up my mess of code.
|
||||
|
||||
KSX for their `Interact in Battle` code.
|
||||
|
||||
RavSpect for their title screen image edit.
|
||||
|
||||
SunCatMC for their work on ChecksFinder, which I used as a basis for game-to-client communication.
|
||||
|
||||
ThePhar for their work on Rogue Legacy AP, which I used as a basis for the apworld creation.
|
||||
54
worlds/kh1/docs/kh1_en.md
Normal file
54
worlds/kh1/docs/kh1_en.md
Normal file
@@ -0,0 +1,54 @@
|
||||
# Kingdom Hearts Randomizer Setup Guide
|
||||
|
||||
## Setting up the required mods
|
||||
|
||||
BEFORE MODDING, PLEASE INSTALL AND RUN KH1 AT LEAST ONCE.
|
||||
|
||||
1. Install OpenKH and the LUA Backend
|
||||
|
||||
Download the [latest release of OpenKH](https://github.com/OpenKH/OpenKh/releases/tag/latest)
|
||||
|
||||
Extract the files to a directory of your choosing.
|
||||
|
||||
Open `OpenKh.Tools.ModsManager.exe` and run first time set up
|
||||
|
||||
When prompted for game edition, choose `PC Release`, select which platform you're using (EGS or Steam), navigate to your `Kingdom Hearts I.5 + II.5` installation folder in the path box and click `Next`
|
||||
|
||||
When prompted, install Panacea, then click `Next`
|
||||
|
||||
When prompted, check KH1 plus any other AP game you play and click `Install and configure LUA backend`, then click `Next`
|
||||
|
||||
Extracting game data for KH1 is unnecessary, but you may want to extract data for KH2 if you plan on playing KH2 AP
|
||||
|
||||
Click `Finish`
|
||||
|
||||
2. Open `OpenKh.Tools.ModsManager.exe`
|
||||
|
||||
3. Click the drop-down menu at the top-right and choose `Kingdom Hearts 1`
|
||||
|
||||
4. Click `Mods>Install a New Mod`
|
||||
|
||||
5. In `Add a new mod from GitHub` paste `gaithern/KH-1FM-AP-LUA`
|
||||
|
||||
6. Click `Install`
|
||||
|
||||
7. Navigate to Mod Loader and click `Build and Run`
|
||||
|
||||
|
||||
## Configuring your YAML file
|
||||
|
||||
### What is a YAML file and why do I need one?
|
||||
|
||||
Your YAML file contains a set of configuration options which provide the generator with information about how it should
|
||||
generate your game. Each player of a multiworld will provide their own YAML file. This setup allows each player to enjoy
|
||||
an experience customized for their taste, and different players in the same multiworld can all have different options.
|
||||
|
||||
### Where do I get a YAML file?
|
||||
|
||||
you can customize your settings by visiting the [Kingdom Hearts Options Page](/games/Kingdom%20Hearts/player-options).
|
||||
|
||||
## Connect to the MultiWorld
|
||||
|
||||
For first-time players, it is recommended to open your KH1 Client first before opening the game.
|
||||
|
||||
On the title screen, open your KH1 Client and connect to your multiworld.
|
||||
5
worlds/kh1/test/__init__.py
Normal file
5
worlds/kh1/test/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from test.bases import WorldTestBase
|
||||
|
||||
|
||||
class KH1TestBase(WorldTestBase):
|
||||
game = "Kingdom Hearts"
|
||||
33
worlds/kh1/test/test_goal.py
Normal file
33
worlds/kh1/test/test_goal.py
Normal file
@@ -0,0 +1,33 @@
|
||||
from . import KH1TestBase
|
||||
|
||||
class TestDefault(KH1TestBase):
|
||||
options = {}
|
||||
|
||||
class TestSephiroth(KH1TestBase):
|
||||
options = {
|
||||
"Goal": 0,
|
||||
}
|
||||
|
||||
class TestUnknown(KH1TestBase):
|
||||
options = {
|
||||
"Goal": 1,
|
||||
}
|
||||
|
||||
class TestPostcards(KH1TestBase):
|
||||
options = {
|
||||
"Goal": 2,
|
||||
}
|
||||
|
||||
class TestFinalAnsem(KH1TestBase):
|
||||
options = {
|
||||
"Goal": 3,
|
||||
}
|
||||
|
||||
class TestPuppies(KH1TestBase):
|
||||
options = {
|
||||
"Goal": 4,
|
||||
}
|
||||
class TestFinalRest(KH1TestBase):
|
||||
options = {
|
||||
"Goal": 5,
|
||||
}
|
||||
@@ -280,6 +280,8 @@ def generateRom(args, world: "LinksAwakeningWorld"):
|
||||
name = "Your"
|
||||
else:
|
||||
name = f"{world.multiworld.player_name[location.item.player]}'s"
|
||||
# filter out { and } since they cause issues with string.format later on
|
||||
name = name.replace("{", "").replace("}", "")
|
||||
|
||||
if isinstance(location, LinksAwakeningLocation):
|
||||
location_name = location.ladxr_item.metadata.name
|
||||
@@ -288,7 +290,9 @@ def generateRom(args, world: "LinksAwakeningWorld"):
|
||||
|
||||
hint = f"{name} {location.item} is at {location_name}"
|
||||
if location.player != world.player:
|
||||
hint += f" in {world.multiworld.player_name[location.player]}'s world"
|
||||
# filter out { and } since they cause issues with string.format later on
|
||||
player_name = world.multiworld.player_name[location.player].replace("{", "").replace("}", "")
|
||||
hint += f" in {player_name}'s world"
|
||||
|
||||
# Cap hint size at 85
|
||||
# Realistically we could go bigger but let's be safe instead
|
||||
|
||||
@@ -18,7 +18,8 @@ class ShopItem(ItemInfo):
|
||||
mw_text = ""
|
||||
if multiworld:
|
||||
mw_text = f" for player {rom.player_names[multiworld - 1].encode('ascii', 'replace').decode()}"
|
||||
|
||||
# filter out { and } since they cause issues with string.format later on
|
||||
mw_text = mw_text.replace("{", "").replace("}", "")
|
||||
|
||||
if self.custom_item_name:
|
||||
name = self.custom_item_name
|
||||
|
||||
@@ -85,7 +85,7 @@ class LingoWorld(World):
|
||||
state.collect(self.create_item(self.player_logic.forced_good_item), True)
|
||||
|
||||
all_locations = self.multiworld.get_locations(self.player)
|
||||
state.sweep_for_events(locations=all_locations)
|
||||
state.sweep_for_advancements(locations=all_locations)
|
||||
|
||||
unreachable_locations = [location for location in all_locations
|
||||
if not state.can_reach_location(location.name, self.player)]
|
||||
|
||||
@@ -1,10 +1,14 @@
|
||||
from math import ceil
|
||||
from typing import List
|
||||
|
||||
from BaseClasses import MultiWorld, Item
|
||||
from worlds.AutoWorld import World
|
||||
from BaseClasses import Item
|
||||
|
||||
from . import Constants
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import MinecraftWorld
|
||||
|
||||
|
||||
def get_junk_item_names(rand, k: int) -> str:
|
||||
junk_weights = Constants.item_info["junk_weights"]
|
||||
@@ -14,39 +18,38 @@ def get_junk_item_names(rand, k: int) -> str:
|
||||
k=k)
|
||||
return junk
|
||||
|
||||
def build_item_pool(mc_world: World) -> List[Item]:
|
||||
multiworld = mc_world.multiworld
|
||||
player = mc_world.player
|
||||
def build_item_pool(world: "MinecraftWorld") -> List[Item]:
|
||||
multiworld = world.multiworld
|
||||
player = world.player
|
||||
|
||||
itempool = []
|
||||
total_location_count = len(multiworld.get_unfilled_locations(player))
|
||||
|
||||
required_pool = Constants.item_info["required_pool"]
|
||||
junk_weights = Constants.item_info["junk_weights"]
|
||||
|
||||
# Add required progression items
|
||||
for item_name, num in required_pool.items():
|
||||
itempool += [mc_world.create_item(item_name) for _ in range(num)]
|
||||
itempool += [world.create_item(item_name) for _ in range(num)]
|
||||
|
||||
# Add structure compasses
|
||||
if multiworld.structure_compasses[player]:
|
||||
compasses = [name for name in mc_world.item_name_to_id if "Structure Compass" in name]
|
||||
if world.options.structure_compasses:
|
||||
compasses = [name for name in world.item_name_to_id if "Structure Compass" in name]
|
||||
for item_name in compasses:
|
||||
itempool.append(mc_world.create_item(item_name))
|
||||
itempool.append(world.create_item(item_name))
|
||||
|
||||
# Dragon egg shards
|
||||
if multiworld.egg_shards_required[player] > 0:
|
||||
num = multiworld.egg_shards_available[player]
|
||||
itempool += [mc_world.create_item("Dragon Egg Shard") for _ in range(num)]
|
||||
if world.options.egg_shards_required > 0:
|
||||
num = world.options.egg_shards_available
|
||||
itempool += [world.create_item("Dragon Egg Shard") for _ in range(num)]
|
||||
|
||||
# Bee traps
|
||||
bee_trap_percentage = multiworld.bee_traps[player] * 0.01
|
||||
bee_trap_percentage = world.options.bee_traps * 0.01
|
||||
if bee_trap_percentage > 0:
|
||||
bee_trap_qty = ceil(bee_trap_percentage * (total_location_count - len(itempool)))
|
||||
itempool += [mc_world.create_item("Bee Trap") for _ in range(bee_trap_qty)]
|
||||
itempool += [world.create_item("Bee Trap") for _ in range(bee_trap_qty)]
|
||||
|
||||
# Fill remaining itempool with randomly generated junk
|
||||
junk = get_junk_item_names(multiworld.random, total_location_count - len(itempool))
|
||||
itempool += [mc_world.create_item(name) for name in junk]
|
||||
junk = get_junk_item_names(world.random, total_location_count - len(itempool))
|
||||
itempool += [world.create_item(name) for name in junk]
|
||||
|
||||
return itempool
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
import typing
|
||||
from Options import Choice, Option, Toggle, DefaultOnToggle, Range, OptionList, DeathLink, PlandoConnections
|
||||
from Options import Choice, Toggle, DefaultOnToggle, Range, OptionList, DeathLink, PlandoConnections, \
|
||||
PerGameCommonOptions
|
||||
from .Constants import region_info
|
||||
from dataclasses import dataclass
|
||||
|
||||
|
||||
class AdvancementGoal(Range):
|
||||
@@ -55,7 +56,7 @@ class StructureCompasses(DefaultOnToggle):
|
||||
display_name = "Structure Compasses"
|
||||
|
||||
|
||||
class BeeTraps(Range):
|
||||
class BeeTraps(Range):
|
||||
"""Replaces a percentage of junk items with bee traps, which spawn multiple angered bees around every player when
|
||||
received."""
|
||||
display_name = "Bee Trap Percentage"
|
||||
@@ -94,7 +95,20 @@ class SendDefeatedMobs(Toggle):
|
||||
|
||||
|
||||
class StartingItems(OptionList):
|
||||
"""Start with these items. Each entry should be of this format: {item: "item_name", amount: #, nbt: "nbt_string"}"""
|
||||
"""Start with these items. Each entry should be of this format: {item: "item_name", amount: #}
|
||||
`item` can include components, and should be in an identical format to a `/give` command with
|
||||
`"` escaped for json reasons.
|
||||
|
||||
`amount` is optional and will default to 1 if omitted.
|
||||
|
||||
example:
|
||||
```
|
||||
starting_items: [
|
||||
{ "item": "minecraft:stick[minecraft:custom_name=\"{'text':'pointy stick'}\"]" },
|
||||
{ "item": "minecraft:arrow[minecraft:rarity=epic]", amount: 64 }
|
||||
]
|
||||
```
|
||||
"""
|
||||
display_name = "Starting Items"
|
||||
|
||||
|
||||
@@ -109,22 +123,21 @@ class MCPlandoConnections(PlandoConnections):
|
||||
return True
|
||||
|
||||
|
||||
minecraft_options: typing.Dict[str, type(Option)] = {
|
||||
"plando_connections": MCPlandoConnections,
|
||||
"advancement_goal": AdvancementGoal,
|
||||
"egg_shards_required": EggShardsRequired,
|
||||
"egg_shards_available": EggShardsAvailable,
|
||||
"required_bosses": BossGoal,
|
||||
@dataclass
|
||||
class MinecraftOptions(PerGameCommonOptions):
|
||||
plando_connections: MCPlandoConnections
|
||||
advancement_goal: AdvancementGoal
|
||||
egg_shards_required: EggShardsRequired
|
||||
egg_shards_available: EggShardsAvailable
|
||||
required_bosses: BossGoal
|
||||
shuffle_structures: ShuffleStructures
|
||||
structure_compasses: StructureCompasses
|
||||
|
||||
"shuffle_structures": ShuffleStructures,
|
||||
"structure_compasses": StructureCompasses,
|
||||
|
||||
"combat_difficulty": CombatDifficulty,
|
||||
"include_hard_advancements": HardAdvancements,
|
||||
"include_unreasonable_advancements": UnreasonableAdvancements,
|
||||
"include_postgame_advancements": PostgameAdvancements,
|
||||
"bee_traps": BeeTraps,
|
||||
"send_defeated_mobs": SendDefeatedMobs,
|
||||
"death_link": DeathLink,
|
||||
"starting_items": StartingItems,
|
||||
}
|
||||
combat_difficulty: CombatDifficulty
|
||||
include_hard_advancements: HardAdvancements
|
||||
include_unreasonable_advancements: UnreasonableAdvancements
|
||||
include_postgame_advancements: PostgameAdvancements
|
||||
bee_traps: BeeTraps
|
||||
send_defeated_mobs: SendDefeatedMobs
|
||||
death_link: DeathLink
|
||||
starting_items: StartingItems
|
||||
|
||||
@@ -1,276 +1,471 @@
|
||||
import typing
|
||||
from collections.abc import Callable
|
||||
|
||||
from BaseClasses import CollectionState
|
||||
from worlds.generic.Rules import exclusion_rules
|
||||
from worlds.AutoWorld import World
|
||||
|
||||
from . import Constants
|
||||
from typing import TYPE_CHECKING
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import MinecraftWorld
|
||||
|
||||
|
||||
# Helper functions
|
||||
# moved from logicmixin
|
||||
|
||||
def has_iron_ingots(state: CollectionState, player: int) -> bool:
|
||||
def has_iron_ingots(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
|
||||
return state.has('Progressive Tools', player) and state.has('Progressive Resource Crafting', player)
|
||||
|
||||
def has_copper_ingots(state: CollectionState, player: int) -> bool:
|
||||
|
||||
def has_copper_ingots(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
|
||||
return state.has('Progressive Tools', player) and state.has('Progressive Resource Crafting', player)
|
||||
|
||||
def has_gold_ingots(state: CollectionState, player: int) -> bool:
|
||||
return state.has('Progressive Resource Crafting', player) and (state.has('Progressive Tools', player, 2) or state.can_reach('The Nether', 'Region', player))
|
||||
|
||||
def has_diamond_pickaxe(state: CollectionState, player: int) -> bool:
|
||||
return state.has('Progressive Tools', player, 3) and has_iron_ingots(state, player)
|
||||
def has_gold_ingots(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
|
||||
return (state.has('Progressive Resource Crafting', player)
|
||||
and (
|
||||
state.has('Progressive Tools', player, 2)
|
||||
or state.can_reach_region('The Nether', player)
|
||||
)
|
||||
)
|
||||
|
||||
def craft_crossbow(state: CollectionState, player: int) -> bool:
|
||||
return state.has('Archery', player) and has_iron_ingots(state, player)
|
||||
|
||||
def has_bottle(state: CollectionState, player: int) -> bool:
|
||||
def has_diamond_pickaxe(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
|
||||
return state.has('Progressive Tools', player, 3) and has_iron_ingots(world, state, player)
|
||||
|
||||
|
||||
def craft_crossbow(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
|
||||
return state.has('Archery', player) and has_iron_ingots(world, state, player)
|
||||
|
||||
|
||||
def has_bottle(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
|
||||
return state.has('Bottles', player) and state.has('Progressive Resource Crafting', player)
|
||||
|
||||
def has_spyglass(state: CollectionState, player: int) -> bool:
|
||||
return has_copper_ingots(state, player) and state.has('Spyglass', player) and can_adventure(state, player)
|
||||
|
||||
def can_enchant(state: CollectionState, player: int) -> bool:
|
||||
return state.has('Enchanting', player) and has_diamond_pickaxe(state, player) # mine obsidian and lapis
|
||||
def has_spyglass(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
|
||||
return (has_copper_ingots(world, state, player)
|
||||
and state.has('Spyglass', player)
|
||||
and can_adventure(world, state, player)
|
||||
)
|
||||
|
||||
def can_use_anvil(state: CollectionState, player: int) -> bool:
|
||||
return state.has('Enchanting', player) and state.has('Progressive Resource Crafting', player, 2) and has_iron_ingots(state, player)
|
||||
|
||||
def fortress_loot(state: CollectionState, player: int) -> bool: # saddles, blaze rods, wither skulls
|
||||
return state.can_reach('Nether Fortress', 'Region', player) and basic_combat(state, player)
|
||||
def can_enchant(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
|
||||
return state.has('Enchanting', player) and has_diamond_pickaxe(world, state, player) # mine obsidian and lapis
|
||||
|
||||
def can_brew_potions(state: CollectionState, player: int) -> bool:
|
||||
return state.has('Blaze Rods', player) and state.has('Brewing', player) and has_bottle(state, player)
|
||||
|
||||
def can_piglin_trade(state: CollectionState, player: int) -> bool:
|
||||
return has_gold_ingots(state, player) and (
|
||||
state.can_reach('The Nether', 'Region', player) or
|
||||
state.can_reach('Bastion Remnant', 'Region', player))
|
||||
def can_use_anvil(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
|
||||
return (state.has('Enchanting', player)
|
||||
and state.has('Progressive Resource Crafting', player,2)
|
||||
and has_iron_ingots(world, state, player)
|
||||
)
|
||||
|
||||
def overworld_villager(state: CollectionState, player: int) -> bool:
|
||||
|
||||
def fortress_loot(world: "MinecraftWorld", state: CollectionState, player: int) -> bool: # saddles, blaze rods, wither skulls
|
||||
return state.can_reach_region('Nether Fortress', player) and basic_combat(world, state, player)
|
||||
|
||||
|
||||
def can_brew_potions(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
|
||||
return state.has('Blaze Rods', player) and state.has('Brewing', player) and has_bottle(world, state, player)
|
||||
|
||||
|
||||
def can_piglin_trade(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
|
||||
return (has_gold_ingots(world, state, player)
|
||||
and (
|
||||
state.can_reach_region('The Nether', player)
|
||||
or state.can_reach_region('Bastion Remnant', player)
|
||||
))
|
||||
|
||||
|
||||
def overworld_villager(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
|
||||
village_region = state.multiworld.get_region('Village', player).entrances[0].parent_region.name
|
||||
if village_region == 'The Nether': # 2 options: cure zombie villager or build portal in village
|
||||
return (state.can_reach('Zombie Doctor', 'Location', player) or
|
||||
(has_diamond_pickaxe(state, player) and state.can_reach('Village', 'Region', player)))
|
||||
if village_region == 'The Nether': # 2 options: cure zombie villager or build portal in village
|
||||
return (state.can_reach_location('Zombie Doctor', player)
|
||||
or (
|
||||
has_diamond_pickaxe(world, state, player)
|
||||
and state.can_reach_region('Village', player)
|
||||
))
|
||||
elif village_region == 'The End':
|
||||
return state.can_reach('Zombie Doctor', 'Location', player)
|
||||
return state.can_reach('Village', 'Region', player)
|
||||
return state.can_reach_location('Zombie Doctor', player)
|
||||
return state.can_reach_region('Village', player)
|
||||
|
||||
def enter_stronghold(state: CollectionState, player: int) -> bool:
|
||||
|
||||
def enter_stronghold(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
|
||||
return state.has('Blaze Rods', player) and state.has('Brewing', player) and state.has('3 Ender Pearls', player)
|
||||
|
||||
|
||||
# Difficulty-dependent functions
|
||||
def combat_difficulty(state: CollectionState, player: int) -> bool:
|
||||
return state.multiworld.combat_difficulty[player].current_key
|
||||
def combat_difficulty(world: "MinecraftWorld", state: CollectionState, player: int) -> str:
|
||||
return world.options.combat_difficulty.current_key
|
||||
|
||||
def can_adventure(state: CollectionState, player: int) -> bool:
|
||||
death_link_check = not state.multiworld.death_link[player] or state.has('Bed', player)
|
||||
if combat_difficulty(state, player) == 'easy':
|
||||
return state.has('Progressive Weapons', player, 2) and has_iron_ingots(state, player) and death_link_check
|
||||
elif combat_difficulty(state, player) == 'hard':
|
||||
|
||||
def can_adventure(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
|
||||
death_link_check = not world.options.death_link or state.has('Bed', player)
|
||||
if combat_difficulty(world, state, player) == 'easy':
|
||||
return state.has('Progressive Weapons', player, 2) and has_iron_ingots(world, state, player) and death_link_check
|
||||
elif combat_difficulty(world, state, player) == 'hard':
|
||||
return True
|
||||
return (state.has('Progressive Weapons', player) and death_link_check and
|
||||
(state.has('Progressive Resource Crafting', player) or state.has('Campfire', player)))
|
||||
return (state.has('Progressive Weapons', player) and death_link_check and
|
||||
(state.has('Progressive Resource Crafting', player) or state.has('Campfire', player)))
|
||||
|
||||
def basic_combat(state: CollectionState, player: int) -> bool:
|
||||
if combat_difficulty(state, player) == 'easy':
|
||||
return state.has('Progressive Weapons', player, 2) and state.has('Progressive Armor', player) and \
|
||||
state.has('Shield', player) and has_iron_ingots(state, player)
|
||||
elif combat_difficulty(state, player) == 'hard':
|
||||
|
||||
def basic_combat(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
|
||||
if combat_difficulty(world, state, player) == 'easy':
|
||||
return (state.has('Progressive Weapons', player, 2)
|
||||
and state.has('Progressive Armor', player)
|
||||
and state.has('Shield', player)
|
||||
and has_iron_ingots(world, state, player)
|
||||
)
|
||||
elif combat_difficulty(world, state, player) == 'hard':
|
||||
return True
|
||||
return state.has('Progressive Weapons', player) and (state.has('Progressive Armor', player) or state.has('Shield', player)) and has_iron_ingots(state, player)
|
||||
return (state.has('Progressive Weapons', player)
|
||||
and (
|
||||
state.has('Progressive Armor', player)
|
||||
or state.has('Shield', player)
|
||||
)
|
||||
and has_iron_ingots(world, state, player)
|
||||
)
|
||||
|
||||
def complete_raid(state: CollectionState, player: int) -> bool:
|
||||
reach_regions = state.can_reach('Village', 'Region', player) and state.can_reach('Pillager Outpost', 'Region', player)
|
||||
if combat_difficulty(state, player) == 'easy':
|
||||
return reach_regions and \
|
||||
state.has('Progressive Weapons', player, 3) and state.has('Progressive Armor', player, 2) and \
|
||||
state.has('Shield', player) and state.has('Archery', player) and \
|
||||
state.has('Progressive Tools', player, 2) and has_iron_ingots(state, player)
|
||||
elif combat_difficulty(state, player) == 'hard': # might be too hard?
|
||||
return reach_regions and state.has('Progressive Weapons', player, 2) and has_iron_ingots(state, player) and \
|
||||
(state.has('Progressive Armor', player) or state.has('Shield', player))
|
||||
return reach_regions and state.has('Progressive Weapons', player, 2) and has_iron_ingots(state, player) and \
|
||||
state.has('Progressive Armor', player) and state.has('Shield', player)
|
||||
|
||||
def can_kill_wither(state: CollectionState, player: int) -> bool:
|
||||
normal_kill = state.has("Progressive Weapons", player, 3) and state.has("Progressive Armor", player, 2) and can_brew_potions(state, player) and can_enchant(state, player)
|
||||
if combat_difficulty(state, player) == 'easy':
|
||||
return fortress_loot(state, player) and normal_kill and state.has('Archery', player)
|
||||
elif combat_difficulty(state, player) == 'hard': # cheese kill using bedrock ceilings
|
||||
return fortress_loot(state, player) and (normal_kill or state.can_reach('The Nether', 'Region', player) or state.can_reach('The End', 'Region', player))
|
||||
return fortress_loot(state, player) and normal_kill
|
||||
def complete_raid(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
|
||||
reach_regions = (state.can_reach_region('Village', player)
|
||||
and state.can_reach_region('Pillager Outpost', player))
|
||||
if combat_difficulty(world, state, player) == 'easy':
|
||||
return (reach_regions
|
||||
and state.has('Progressive Weapons', player, 3)
|
||||
and state.has('Progressive Armor', player, 2)
|
||||
and state.has('Shield', player)
|
||||
and state.has('Archery', player)
|
||||
and state.has('Progressive Tools', player, 2)
|
||||
and has_iron_ingots(world, state, player)
|
||||
)
|
||||
elif combat_difficulty(world, state, player) == 'hard': # might be too hard?
|
||||
return (reach_regions
|
||||
and state.has('Progressive Weapons', player, 2)
|
||||
and has_iron_ingots(world, state, player)
|
||||
and (
|
||||
state.has('Progressive Armor', player)
|
||||
or state.has('Shield', player)
|
||||
)
|
||||
)
|
||||
return (reach_regions
|
||||
and state.has('Progressive Weapons', player, 2)
|
||||
and has_iron_ingots(world, state, player)
|
||||
and state.has('Progressive Armor', player)
|
||||
and state.has('Shield', player)
|
||||
)
|
||||
|
||||
def can_respawn_ender_dragon(state: CollectionState, player: int) -> bool:
|
||||
return state.can_reach('The Nether', 'Region', player) and state.can_reach('The End', 'Region', player) and \
|
||||
state.has('Progressive Resource Crafting', player) # smelt sand into glass
|
||||
|
||||
def can_kill_ender_dragon(state: CollectionState, player: int) -> bool:
|
||||
if combat_difficulty(state, player) == 'easy':
|
||||
return state.has("Progressive Weapons", player, 3) and state.has("Progressive Armor", player, 2) and \
|
||||
state.has('Archery', player) and can_brew_potions(state, player) and can_enchant(state, player)
|
||||
if combat_difficulty(state, player) == 'hard':
|
||||
return (state.has('Progressive Weapons', player, 2) and state.has('Progressive Armor', player)) or \
|
||||
(state.has('Progressive Weapons', player, 1) and state.has('Bed', player))
|
||||
return state.has('Progressive Weapons', player, 2) and state.has('Progressive Armor', player) and state.has('Archery', player)
|
||||
def can_kill_wither(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
|
||||
normal_kill = (state.has("Progressive Weapons", player, 3)
|
||||
and state.has("Progressive Armor", player, 2)
|
||||
and can_brew_potions(world, state, player)
|
||||
and can_enchant(world, state, player)
|
||||
)
|
||||
if combat_difficulty(world, state, player) == 'easy':
|
||||
return (fortress_loot(world, state, player)
|
||||
and normal_kill
|
||||
and state.has('Archery', player)
|
||||
)
|
||||
elif combat_difficulty(world, state, player) == 'hard': # cheese kill using bedrock ceilings
|
||||
return (fortress_loot(world, state, player)
|
||||
and (
|
||||
normal_kill
|
||||
or state.can_reach_region('The Nether', player)
|
||||
or state.can_reach_region('The End', player)
|
||||
)
|
||||
)
|
||||
|
||||
def has_structure_compass(state: CollectionState, entrance_name: str, player: int) -> bool:
|
||||
if not state.multiworld.structure_compasses[player]:
|
||||
return fortress_loot(world, state, player) and normal_kill
|
||||
|
||||
|
||||
def can_respawn_ender_dragon(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
|
||||
return (state.can_reach_region('The Nether', player)
|
||||
and state.can_reach_region('The End', player)
|
||||
and state.has('Progressive Resource Crafting', player) # smelt sand into glass
|
||||
)
|
||||
|
||||
|
||||
def can_kill_ender_dragon(world: "MinecraftWorld", state: CollectionState, player: int) -> bool:
|
||||
if combat_difficulty(world, state, player) == 'easy':
|
||||
return (state.has("Progressive Weapons", player, 3)
|
||||
and state.has("Progressive Armor", player, 2)
|
||||
and state.has('Archery', player)
|
||||
and can_brew_potions(world, state, player)
|
||||
and can_enchant(world, state, player)
|
||||
)
|
||||
if combat_difficulty(world, state, player) == 'hard':
|
||||
return (
|
||||
(
|
||||
state.has('Progressive Weapons', player, 2)
|
||||
and state.has('Progressive Armor', player)
|
||||
) or (
|
||||
state.has('Progressive Weapons', player, 1)
|
||||
and state.has('Bed', player) # who needs armor when you can respawn right outside the chamber
|
||||
)
|
||||
)
|
||||
return (state.has('Progressive Weapons', player, 2)
|
||||
and state.has('Progressive Armor', player)
|
||||
and state.has('Archery', player)
|
||||
)
|
||||
|
||||
|
||||
def has_structure_compass(world: "MinecraftWorld", state: CollectionState, entrance_name: str, player: int) -> bool:
|
||||
if not world.options.structure_compasses:
|
||||
return True
|
||||
return state.has(f"Structure Compass ({state.multiworld.get_entrance(entrance_name, player).connected_region.name})", player)
|
||||
|
||||
|
||||
def get_rules_lookup(player: int):
|
||||
rules_lookup: typing.Dict[str, typing.List[Callable[[CollectionState], bool]]] = {
|
||||
def get_rules_lookup(world, player: int):
|
||||
rules_lookup = {
|
||||
"entrances": {
|
||||
"Nether Portal": lambda state: (state.has('Flint and Steel', player) and
|
||||
(state.has('Bucket', player) or state.has('Progressive Tools', player, 3)) and
|
||||
has_iron_ingots(state, player)),
|
||||
"End Portal": lambda state: enter_stronghold(state, player) and state.has('3 Ender Pearls', player, 4),
|
||||
"Overworld Structure 1": lambda state: (can_adventure(state, player) and has_structure_compass(state, "Overworld Structure 1", player)),
|
||||
"Overworld Structure 2": lambda state: (can_adventure(state, player) and has_structure_compass(state, "Overworld Structure 2", player)),
|
||||
"Nether Structure 1": lambda state: (can_adventure(state, player) and has_structure_compass(state, "Nether Structure 1", player)),
|
||||
"Nether Structure 2": lambda state: (can_adventure(state, player) and has_structure_compass(state, "Nether Structure 2", player)),
|
||||
"The End Structure": lambda state: (can_adventure(state, player) and has_structure_compass(state, "The End Structure", player)),
|
||||
"Nether Portal": lambda state: state.has('Flint and Steel', player)
|
||||
and (
|
||||
state.has('Bucket', player)
|
||||
or state.has('Progressive Tools', player, 3)
|
||||
)
|
||||
and has_iron_ingots(world, state, player),
|
||||
"End Portal": lambda state: enter_stronghold(world, state, player)
|
||||
and state.has('3 Ender Pearls', player, 4),
|
||||
"Overworld Structure 1": lambda state: can_adventure(world, state, player)
|
||||
and has_structure_compass(world, state, "Overworld Structure 1", player),
|
||||
"Overworld Structure 2": lambda state: can_adventure(world, state, player)
|
||||
and has_structure_compass(world, state, "Overworld Structure 2", player),
|
||||
"Nether Structure 1": lambda state: can_adventure(world, state, player)
|
||||
and has_structure_compass(world, state, "Nether Structure 1", player),
|
||||
"Nether Structure 2": lambda state: can_adventure(world, state, player)
|
||||
and has_structure_compass(world, state, "Nether Structure 2", player),
|
||||
"The End Structure": lambda state: can_adventure(world, state, player)
|
||||
and has_structure_compass(world, state, "The End Structure", player),
|
||||
},
|
||||
"locations": {
|
||||
"Ender Dragon": lambda state: can_respawn_ender_dragon(state, player) and can_kill_ender_dragon(state, player),
|
||||
"Wither": lambda state: can_kill_wither(state, player),
|
||||
"Blaze Rods": lambda state: fortress_loot(state, player),
|
||||
|
||||
"Who is Cutting Onions?": lambda state: can_piglin_trade(state, player),
|
||||
"Oh Shiny": lambda state: can_piglin_trade(state, player),
|
||||
"Suit Up": lambda state: state.has("Progressive Armor", player) and has_iron_ingots(state, player),
|
||||
"Very Very Frightening": lambda state: (state.has("Channeling Book", player) and
|
||||
can_use_anvil(state, player) and can_enchant(state, player) and overworld_villager(state, player)),
|
||||
"Hot Stuff": lambda state: state.has("Bucket", player) and has_iron_ingots(state, player),
|
||||
"Free the End": lambda state: can_respawn_ender_dragon(state, player) and can_kill_ender_dragon(state, player),
|
||||
"A Furious Cocktail": lambda state: (can_brew_potions(state, player) and
|
||||
state.has("Fishing Rod", player) and # Water Breathing
|
||||
state.can_reach("The Nether", "Region", player) and # Regeneration, Fire Resistance, gold nuggets
|
||||
state.can_reach("Village", "Region", player) and # Night Vision, Invisibility
|
||||
state.can_reach("Bring Home the Beacon", "Location", player)), # Resistance
|
||||
"Bring Home the Beacon": lambda state: (can_kill_wither(state, player) and
|
||||
has_diamond_pickaxe(state, player) and state.has("Progressive Resource Crafting", player, 2)),
|
||||
"Not Today, Thank You": lambda state: state.has("Shield", player) and has_iron_ingots(state, player),
|
||||
"Isn't It Iron Pick": lambda state: state.has("Progressive Tools", player, 2) and has_iron_ingots(state, player),
|
||||
"Local Brewery": lambda state: can_brew_potions(state, player),
|
||||
"The Next Generation": lambda state: can_respawn_ender_dragon(state, player) and can_kill_ender_dragon(state, player),
|
||||
"Ender Dragon": lambda state: can_respawn_ender_dragon(world, state, player)
|
||||
and can_kill_ender_dragon(world, state, player),
|
||||
"Wither": lambda state: can_kill_wither(world, state, player),
|
||||
"Blaze Rods": lambda state: fortress_loot(world, state, player),
|
||||
"Who is Cutting Onions?": lambda state: can_piglin_trade(world, state, player),
|
||||
"Oh Shiny": lambda state: can_piglin_trade(world, state, player),
|
||||
"Suit Up": lambda state: state.has("Progressive Armor", player)
|
||||
and has_iron_ingots(world, state, player),
|
||||
"Very Very Frightening": lambda state: state.has("Channeling Book", player)
|
||||
and can_use_anvil(world, state, player)
|
||||
and can_enchant(world, state, player)
|
||||
and overworld_villager(world, state, player),
|
||||
"Hot Stuff": lambda state: state.has("Bucket", player)
|
||||
and has_iron_ingots(world, state, player),
|
||||
"Free the End": lambda state: can_respawn_ender_dragon(world, state, player)
|
||||
and can_kill_ender_dragon(world, state, player),
|
||||
"A Furious Cocktail": lambda state: (can_brew_potions(world, state, player)
|
||||
and state.has("Fishing Rod", player) # Water Breathing
|
||||
and state.can_reach_region("The Nether", player) # Regeneration, Fire Resistance, gold nuggets
|
||||
and state.can_reach_region("Village", player) # Night Vision, Invisibility
|
||||
and state.can_reach_location("Bring Home the Beacon", player)),
|
||||
# Resistance
|
||||
"Bring Home the Beacon": lambda state: can_kill_wither(world, state, player)
|
||||
and has_diamond_pickaxe(world, state, player)
|
||||
and state.has("Progressive Resource Crafting", player, 2),
|
||||
"Not Today, Thank You": lambda state: state.has("Shield", player)
|
||||
and has_iron_ingots(world, state, player),
|
||||
"Isn't It Iron Pick": lambda state: state.has("Progressive Tools", player, 2)
|
||||
and has_iron_ingots(world, state, player),
|
||||
"Local Brewery": lambda state: can_brew_potions(world, state, player),
|
||||
"The Next Generation": lambda state: can_respawn_ender_dragon(world, state, player)
|
||||
and can_kill_ender_dragon(world, state, player),
|
||||
"Fishy Business": lambda state: state.has("Fishing Rod", player),
|
||||
"This Boat Has Legs": lambda state: ((fortress_loot(state, player) or complete_raid(state, player)) and
|
||||
state.has("Saddle", player) and state.has("Fishing Rod", player)),
|
||||
"This Boat Has Legs": lambda state: (
|
||||
fortress_loot(world, state, player)
|
||||
or complete_raid(world, state, player)
|
||||
)
|
||||
and state.has("Saddle", player)
|
||||
and state.has("Fishing Rod", player),
|
||||
"Sniper Duel": lambda state: state.has("Archery", player),
|
||||
"Great View From Up Here": lambda state: basic_combat(state, player),
|
||||
"How Did We Get Here?": lambda state: (can_brew_potions(state, player) and
|
||||
has_gold_ingots(state, player) and # Absorption
|
||||
state.can_reach('End City', 'Region', player) and # Levitation
|
||||
state.can_reach('The Nether', 'Region', player) and # potion ingredients
|
||||
state.has("Fishing Rod", player) and state.has("Archery",player) and # Pufferfish, Nautilus Shells; spectral arrows
|
||||
state.can_reach("Bring Home the Beacon", "Location", player) and # Haste
|
||||
state.can_reach("Hero of the Village", "Location", player)), # Bad Omen, Hero of the Village
|
||||
"Bullseye": lambda state: (state.has("Archery", player) and state.has("Progressive Tools", player, 2) and
|
||||
has_iron_ingots(state, player)),
|
||||
"Spooky Scary Skeleton": lambda state: basic_combat(state, player),
|
||||
"Two by Two": lambda state: has_iron_ingots(state, player) and state.has("Bucket", player) and can_adventure(state, player),
|
||||
"Two Birds, One Arrow": lambda state: craft_crossbow(state, player) and can_enchant(state, player),
|
||||
"Who's the Pillager Now?": lambda state: craft_crossbow(state, player),
|
||||
"Great View From Up Here": lambda state: basic_combat(world, state, player),
|
||||
"How Did We Get Here?": lambda state: (can_brew_potions(world, state, player)
|
||||
and has_gold_ingots(world, state, player) # Absorption
|
||||
and state.can_reach_region('End City', player) # Levitation
|
||||
and state.can_reach_region('The Nether', player) # potion ingredients
|
||||
and state.has("Fishing Rod", player) # Pufferfish, Nautilus Shells; spectral arrows
|
||||
and state.has("Archery", player)
|
||||
and state.can_reach_location("Bring Home the Beacon", player) # Haste
|
||||
and state.can_reach_location("Hero of the Village", player)), # Bad Omen, Hero of the Village
|
||||
"Bullseye": lambda state: state.has("Archery", player)
|
||||
and state.has("Progressive Tools", player, 2)
|
||||
and has_iron_ingots(world, state, player),
|
||||
"Spooky Scary Skeleton": lambda state: basic_combat(world, state, player),
|
||||
"Two by Two": lambda state: has_iron_ingots(world, state, player)
|
||||
and state.has("Bucket", player)
|
||||
and can_adventure(world, state, player),
|
||||
"Two Birds, One Arrow": lambda state: craft_crossbow(world, state, player)
|
||||
and can_enchant(world, state, player),
|
||||
"Who's the Pillager Now?": lambda state: craft_crossbow(world, state, player),
|
||||
"Getting an Upgrade": lambda state: state.has("Progressive Tools", player),
|
||||
"Tactical Fishing": lambda state: state.has("Bucket", player) and has_iron_ingots(state, player),
|
||||
"Zombie Doctor": lambda state: can_brew_potions(state, player) and has_gold_ingots(state, player),
|
||||
"Ice Bucket Challenge": lambda state: has_diamond_pickaxe(state, player),
|
||||
"Into Fire": lambda state: basic_combat(state, player),
|
||||
"War Pigs": lambda state: basic_combat(state, player),
|
||||
"Tactical Fishing": lambda state: state.has("Bucket", player)
|
||||
and has_iron_ingots(world, state, player),
|
||||
"Zombie Doctor": lambda state: can_brew_potions(world, state, player)
|
||||
and has_gold_ingots(world, state, player),
|
||||
"Ice Bucket Challenge": lambda state: has_diamond_pickaxe(world, state, player),
|
||||
"Into Fire": lambda state: basic_combat(world, state, player),
|
||||
"War Pigs": lambda state: basic_combat(world, state, player),
|
||||
"Take Aim": lambda state: state.has("Archery", player),
|
||||
"Total Beelocation": lambda state: state.has("Silk Touch Book", player) and can_use_anvil(state, player) and can_enchant(state, player),
|
||||
"Arbalistic": lambda state: (craft_crossbow(state, player) and state.has("Piercing IV Book", player) and
|
||||
can_use_anvil(state, player) and can_enchant(state, player)),
|
||||
"The End... Again...": lambda state: can_respawn_ender_dragon(state, player) and can_kill_ender_dragon(state, player),
|
||||
"Acquire Hardware": lambda state: has_iron_ingots(state, player),
|
||||
"Not Quite \"Nine\" Lives": lambda state: can_piglin_trade(state, player) and state.has("Progressive Resource Crafting", player, 2),
|
||||
"Cover Me With Diamonds": lambda state: (state.has("Progressive Armor", player, 2) and
|
||||
state.has("Progressive Tools", player, 2) and has_iron_ingots(state, player)),
|
||||
"Sky's the Limit": lambda state: basic_combat(state, player),
|
||||
"Hired Help": lambda state: state.has("Progressive Resource Crafting", player, 2) and has_iron_ingots(state, player),
|
||||
"Sweet Dreams": lambda state: state.has("Bed", player) or state.can_reach('Village', 'Region', player),
|
||||
"You Need a Mint": lambda state: can_respawn_ender_dragon(state, player) and has_bottle(state, player),
|
||||
"Monsters Hunted": lambda state: (can_respawn_ender_dragon(state, player) and can_kill_ender_dragon(state, player) and
|
||||
can_kill_wither(state, player) and state.has("Fishing Rod", player)),
|
||||
"Enchanter": lambda state: can_enchant(state, player),
|
||||
"Voluntary Exile": lambda state: basic_combat(state, player),
|
||||
"Eye Spy": lambda state: enter_stronghold(state, player),
|
||||
"Serious Dedication": lambda state: (can_brew_potions(state, player) and state.has("Bed", player) and
|
||||
has_diamond_pickaxe(state, player) and has_gold_ingots(state, player)),
|
||||
"Postmortal": lambda state: complete_raid(state, player),
|
||||
"Adventuring Time": lambda state: can_adventure(state, player),
|
||||
"Hero of the Village": lambda state: complete_raid(state, player),
|
||||
"Hidden in the Depths": lambda state: can_brew_potions(state, player) and state.has("Bed", player) and has_diamond_pickaxe(state, player),
|
||||
"Beaconator": lambda state: (can_kill_wither(state, player) and has_diamond_pickaxe(state, player) and
|
||||
state.has("Progressive Resource Crafting", player, 2)),
|
||||
"Withering Heights": lambda state: can_kill_wither(state, player),
|
||||
"A Balanced Diet": lambda state: (has_bottle(state, player) and has_gold_ingots(state, player) and # honey bottle; gapple
|
||||
state.has("Progressive Resource Crafting", player, 2) and state.can_reach('The End', 'Region', player)), # notch apple, chorus fruit
|
||||
"Subspace Bubble": lambda state: has_diamond_pickaxe(state, player),
|
||||
"Country Lode, Take Me Home": lambda state: state.can_reach("Hidden in the Depths", "Location", player) and has_gold_ingots(state, player),
|
||||
"Bee Our Guest": lambda state: state.has("Campfire", player) and has_bottle(state, player),
|
||||
"Uneasy Alliance": lambda state: has_diamond_pickaxe(state, player) and state.has('Fishing Rod', player),
|
||||
"Diamonds!": lambda state: state.has("Progressive Tools", player, 2) and has_iron_ingots(state, player),
|
||||
"A Throwaway Joke": lambda state: can_adventure(state, player),
|
||||
"Sticky Situation": lambda state: state.has("Campfire", player) and has_bottle(state, player),
|
||||
"Ol' Betsy": lambda state: craft_crossbow(state, player),
|
||||
"Cover Me in Debris": lambda state: (state.has("Progressive Armor", player, 2) and
|
||||
state.has("8 Netherite Scrap", player, 2) and state.has("Progressive Resource Crafting", player) and
|
||||
has_diamond_pickaxe(state, player) and has_iron_ingots(state, player) and
|
||||
can_brew_potions(state, player) and state.has("Bed", player)),
|
||||
"Total Beelocation": lambda state: state.has("Silk Touch Book", player)
|
||||
and can_use_anvil(world, state, player)
|
||||
and can_enchant(world, state, player),
|
||||
"Arbalistic": lambda state: (craft_crossbow(world, state, player)
|
||||
and state.has("Piercing IV Book", player)
|
||||
and can_use_anvil(world, state, player)
|
||||
and can_enchant(world, state, player)
|
||||
),
|
||||
"The End... Again...": lambda state: can_respawn_ender_dragon(world, state, player)
|
||||
and can_kill_ender_dragon(world, state, player),
|
||||
"Acquire Hardware": lambda state: has_iron_ingots(world, state, player),
|
||||
"Not Quite \"Nine\" Lives": lambda state: can_piglin_trade(world, state, player)
|
||||
and state.has("Progressive Resource Crafting", player, 2),
|
||||
"Cover Me With Diamonds": lambda state: state.has("Progressive Armor", player, 2)
|
||||
and state.has("Progressive Tools", player, 2)
|
||||
and has_iron_ingots(world, state, player),
|
||||
"Sky's the Limit": lambda state: basic_combat(world, state, player),
|
||||
"Hired Help": lambda state: state.has("Progressive Resource Crafting", player, 2)
|
||||
and has_iron_ingots(world, state, player),
|
||||
"Sweet Dreams": lambda state: state.has("Bed", player)
|
||||
or state.can_reach_region('Village', player),
|
||||
"You Need a Mint": lambda state: can_respawn_ender_dragon(world, state, player)
|
||||
and has_bottle(world, state, player),
|
||||
"Monsters Hunted": lambda state: (can_respawn_ender_dragon(world, state, player)
|
||||
and can_kill_ender_dragon(world, state, player)
|
||||
and can_kill_wither(world, state, player)
|
||||
and state.has("Fishing Rod", player)),
|
||||
"Enchanter": lambda state: can_enchant(world, state, player),
|
||||
"Voluntary Exile": lambda state: basic_combat(world, state, player),
|
||||
"Eye Spy": lambda state: enter_stronghold(world, state, player),
|
||||
"Serious Dedication": lambda state: (can_brew_potions(world, state, player)
|
||||
and state.has("Bed", player)
|
||||
and has_diamond_pickaxe(world, state, player)
|
||||
and has_gold_ingots(world, state, player)),
|
||||
"Postmortal": lambda state: complete_raid(world, state, player),
|
||||
"Adventuring Time": lambda state: can_adventure(world, state, player),
|
||||
"Hero of the Village": lambda state: complete_raid(world, state, player),
|
||||
"Hidden in the Depths": lambda state: can_brew_potions(world, state, player)
|
||||
and state.has("Bed", player)
|
||||
and has_diamond_pickaxe(world, state, player),
|
||||
"Beaconator": lambda state: (can_kill_wither(world, state, player)
|
||||
and has_diamond_pickaxe(world, state, player)
|
||||
and state.has("Progressive Resource Crafting", player, 2)),
|
||||
"Withering Heights": lambda state: can_kill_wither(world, state, player),
|
||||
"A Balanced Diet": lambda state: (has_bottle(world, state, player)
|
||||
and has_gold_ingots(world, state, player)
|
||||
and state.has("Progressive Resource Crafting", player, 2)
|
||||
and state.can_reach_region('The End', player)),
|
||||
# notch apple, chorus fruit
|
||||
"Subspace Bubble": lambda state: has_diamond_pickaxe(world, state, player),
|
||||
"Country Lode, Take Me Home": lambda state: state.can_reach_location("Hidden in the Depths", player)
|
||||
and has_gold_ingots(world, state, player),
|
||||
"Bee Our Guest": lambda state: state.has("Campfire", player)
|
||||
and has_bottle(world, state, player),
|
||||
"Uneasy Alliance": lambda state: has_diamond_pickaxe(world, state, player)
|
||||
and state.has('Fishing Rod', player),
|
||||
"Diamonds!": lambda state: state.has("Progressive Tools", player, 2)
|
||||
and has_iron_ingots(world, state, player),
|
||||
"A Throwaway Joke": lambda state: can_adventure(world, state, player),
|
||||
"Sticky Situation": lambda state: state.has("Campfire", player)
|
||||
and has_bottle(world, state, player),
|
||||
"Ol' Betsy": lambda state: craft_crossbow(world, state, player),
|
||||
"Cover Me in Debris": lambda state: state.has("Progressive Armor", player, 2)
|
||||
and state.has("8 Netherite Scrap", player, 2)
|
||||
and state.has("Progressive Resource Crafting", player)
|
||||
and has_diamond_pickaxe(world, state, player)
|
||||
and has_iron_ingots(world, state, player)
|
||||
and can_brew_potions(world, state, player)
|
||||
and state.has("Bed", player),
|
||||
"Hot Topic": lambda state: state.has("Progressive Resource Crafting", player),
|
||||
"The Lie": lambda state: has_iron_ingots(state, player) and state.has("Bucket", player),
|
||||
"On a Rail": lambda state: has_iron_ingots(state, player) and state.has('Progressive Tools', player, 2),
|
||||
"When Pigs Fly": lambda state: ((fortress_loot(state, player) or complete_raid(state, player)) and
|
||||
state.has("Saddle", player) and state.has("Fishing Rod", player) and can_adventure(state, player)),
|
||||
"Overkill": lambda state: (can_brew_potions(state, player) and
|
||||
(state.has("Progressive Weapons", player) or state.can_reach('The Nether', 'Region', player))),
|
||||
"The Lie": lambda state: has_iron_ingots(world, state, player)
|
||||
and state.has("Bucket", player),
|
||||
"On a Rail": lambda state: has_iron_ingots(world, state, player)
|
||||
and state.has('Progressive Tools', player, 2),
|
||||
"When Pigs Fly": lambda state: (
|
||||
fortress_loot(world, state, player)
|
||||
or complete_raid(world, state, player)
|
||||
)
|
||||
and state.has("Saddle", player)
|
||||
and state.has("Fishing Rod", player)
|
||||
and can_adventure(world, state, player),
|
||||
"Overkill": lambda state: can_brew_potions(world, state, player)
|
||||
and (
|
||||
state.has("Progressive Weapons", player)
|
||||
or state.can_reach_region('The Nether', player)
|
||||
),
|
||||
"Librarian": lambda state: state.has("Enchanting", player),
|
||||
"Overpowered": lambda state: (has_iron_ingots(state, player) and
|
||||
state.has('Progressive Tools', player, 2) and basic_combat(state, player)),
|
||||
"Wax On": lambda state: (has_copper_ingots(state, player) and state.has('Campfire', player) and
|
||||
state.has('Progressive Resource Crafting', player, 2)),
|
||||
"Wax Off": lambda state: (has_copper_ingots(state, player) and state.has('Campfire', player) and
|
||||
state.has('Progressive Resource Crafting', player, 2)),
|
||||
"The Cutest Predator": lambda state: has_iron_ingots(state, player) and state.has('Bucket', player),
|
||||
"The Healing Power of Friendship": lambda state: has_iron_ingots(state, player) and state.has('Bucket', player),
|
||||
"Is It a Bird?": lambda state: has_spyglass(state, player) and can_adventure(state, player),
|
||||
"Is It a Balloon?": lambda state: has_spyglass(state, player),
|
||||
"Is It a Plane?": lambda state: has_spyglass(state, player) and can_respawn_ender_dragon(state, player),
|
||||
"Surge Protector": lambda state: (state.has("Channeling Book", player) and
|
||||
can_use_anvil(state, player) and can_enchant(state, player) and overworld_villager(state, player)),
|
||||
"Light as a Rabbit": lambda state: can_adventure(state, player) and has_iron_ingots(state, player) and state.has('Bucket', player),
|
||||
"Glow and Behold!": lambda state: can_adventure(state, player),
|
||||
"Whatever Floats Your Goat!": lambda state: can_adventure(state, player),
|
||||
"Caves & Cliffs": lambda state: has_iron_ingots(state, player) and state.has('Bucket', player) and state.has('Progressive Tools', player, 2),
|
||||
"Feels like home": lambda state: (has_iron_ingots(state, player) and state.has('Bucket', player) and state.has('Fishing Rod', player) and
|
||||
(fortress_loot(state, player) or complete_raid(state, player)) and state.has("Saddle", player)),
|
||||
"Sound of Music": lambda state: state.has("Progressive Tools", player, 2) and has_iron_ingots(state, player) and basic_combat(state, player),
|
||||
"Star Trader": lambda state: (has_iron_ingots(state, player) and state.has('Bucket', player) and
|
||||
(state.can_reach("The Nether", 'Region', player) or
|
||||
state.can_reach("Nether Fortress", 'Region', player) or # soul sand for water elevator
|
||||
can_piglin_trade(state, player)) and
|
||||
overworld_villager(state, player)),
|
||||
"Birthday Song": lambda state: state.can_reach("The Lie", "Location", player) and state.has("Progressive Tools", player, 2) and has_iron_ingots(state, player),
|
||||
"Bukkit Bukkit": lambda state: state.has("Bucket", player) and has_iron_ingots(state, player) and can_adventure(state, player),
|
||||
"It Spreads": lambda state: can_adventure(state, player) and has_iron_ingots(state, player) and state.has("Progressive Tools", player, 2),
|
||||
"Sneak 100": lambda state: can_adventure(state, player) and has_iron_ingots(state, player) and state.has("Progressive Tools", player, 2),
|
||||
"When the Squad Hops into Town": lambda state: can_adventure(state, player) and state.has("Lead", player),
|
||||
"With Our Powers Combined!": lambda state: can_adventure(state, player) and state.has("Lead", player),
|
||||
"Overpowered": lambda state: has_iron_ingots(world, state, player)
|
||||
and state.has('Progressive Tools', player, 2)
|
||||
and basic_combat(world, state, player),
|
||||
"Wax On": lambda state: has_copper_ingots(world, state, player)
|
||||
and state.has('Campfire', player)
|
||||
and state.has('Progressive Resource Crafting', player, 2),
|
||||
"Wax Off": lambda state: has_copper_ingots(world, state, player)
|
||||
and state.has('Campfire', player)
|
||||
and state.has('Progressive Resource Crafting', player, 2),
|
||||
"The Cutest Predator": lambda state: has_iron_ingots(world, state, player)
|
||||
and state.has('Bucket', player),
|
||||
"The Healing Power of Friendship": lambda state: has_iron_ingots(world, state, player)
|
||||
and state.has('Bucket', player),
|
||||
"Is It a Bird?": lambda state: has_spyglass(world, state, player)
|
||||
and can_adventure(world, state, player),
|
||||
"Is It a Balloon?": lambda state: has_spyglass(world, state, player),
|
||||
"Is It a Plane?": lambda state: has_spyglass(world, state, player)
|
||||
and can_respawn_ender_dragon(world, state, player),
|
||||
"Surge Protector": lambda state: state.has("Channeling Book", player)
|
||||
and can_use_anvil(world, state, player)
|
||||
and can_enchant(world, state, player)
|
||||
and overworld_villager(world, state, player),
|
||||
"Light as a Rabbit": lambda state: can_adventure(world, state, player)
|
||||
and has_iron_ingots(world, state, player)
|
||||
and state.has('Bucket', player),
|
||||
"Glow and Behold!": lambda state: can_adventure(world, state, player),
|
||||
"Whatever Floats Your Goat!": lambda state: can_adventure(world, state, player),
|
||||
"Caves & Cliffs": lambda state: has_iron_ingots(world, state, player)
|
||||
and state.has('Bucket', player)
|
||||
and state.has('Progressive Tools', player, 2),
|
||||
"Feels like home": lambda state: has_iron_ingots(world, state, player)
|
||||
and state.has('Bucket', player)
|
||||
and state.has('Fishing Rod', player)
|
||||
and (
|
||||
fortress_loot(world, state, player)
|
||||
or complete_raid(world, state, player)
|
||||
)
|
||||
and state.has("Saddle", player),
|
||||
"Sound of Music": lambda state: state.has("Progressive Tools", player, 2)
|
||||
and has_iron_ingots(world, state, player)
|
||||
and basic_combat(world, state, player),
|
||||
"Star Trader": lambda state: has_iron_ingots(world, state, player)
|
||||
and state.has('Bucket', player)
|
||||
and (
|
||||
state.can_reach_region("The Nether", player) # soul sand in nether
|
||||
or state.can_reach_region("Nether Fortress", player) # soul sand in fortress if not in nether for water elevator
|
||||
or can_piglin_trade(world, state, player) # piglins give soul sand
|
||||
)
|
||||
and overworld_villager(world, state, player),
|
||||
"Birthday Song": lambda state: state.can_reach_location("The Lie", player)
|
||||
and state.has("Progressive Tools", player, 2)
|
||||
and has_iron_ingots(world, state, player),
|
||||
"Bukkit Bukkit": lambda state: state.has("Bucket", player)
|
||||
and has_iron_ingots(world, state, player)
|
||||
and can_adventure(world, state, player),
|
||||
"It Spreads": lambda state: can_adventure(world, state, player)
|
||||
and has_iron_ingots(world, state, player)
|
||||
and state.has("Progressive Tools", player, 2),
|
||||
"Sneak 100": lambda state: can_adventure(world, state, player)
|
||||
and has_iron_ingots(world, state, player)
|
||||
and state.has("Progressive Tools", player, 2),
|
||||
"When the Squad Hops into Town": lambda state: can_adventure(world, state, player)
|
||||
and state.has("Lead", player),
|
||||
"With Our Powers Combined!": lambda state: can_adventure(world, state, player)
|
||||
and state.has("Lead", player),
|
||||
}
|
||||
}
|
||||
return rules_lookup
|
||||
|
||||
|
||||
def set_rules(mc_world: World) -> None:
|
||||
multiworld = mc_world.multiworld
|
||||
player = mc_world.player
|
||||
def set_rules(self: "MinecraftWorld") -> None:
|
||||
multiworld = self.multiworld
|
||||
player = self.player
|
||||
|
||||
rules_lookup = get_rules_lookup(player)
|
||||
rules_lookup = get_rules_lookup(self, player)
|
||||
|
||||
# Set entrance rules
|
||||
for entrance_name, rule in rules_lookup["entrances"].items():
|
||||
@@ -281,33 +476,33 @@ def set_rules(mc_world: World) -> None:
|
||||
multiworld.get_location(location_name, player).access_rule = rule
|
||||
|
||||
# Set rules surrounding completion
|
||||
bosses = multiworld.required_bosses[player]
|
||||
bosses = self.options.required_bosses
|
||||
postgame_advancements = set()
|
||||
if bosses.dragon:
|
||||
postgame_advancements.update(Constants.exclusion_info["ender_dragon"])
|
||||
if bosses.wither:
|
||||
postgame_advancements.update(Constants.exclusion_info["wither"])
|
||||
|
||||
def location_count(state: CollectionState) -> bool:
|
||||
def location_count(state: CollectionState) -> int:
|
||||
return len([location for location in multiworld.get_locations(player) if
|
||||
location.address != None and
|
||||
location.can_reach(state)])
|
||||
location.address is not None and
|
||||
location.can_reach(state)])
|
||||
|
||||
def defeated_bosses(state: CollectionState) -> bool:
|
||||
return ((not bosses.dragon or state.has("Ender Dragon", player))
|
||||
and (not bosses.wither or state.has("Wither", player)))
|
||||
and (not bosses.wither or state.has("Wither", player)))
|
||||
|
||||
egg_shards = min(multiworld.egg_shards_required[player], multiworld.egg_shards_available[player])
|
||||
completion_requirements = lambda state: (location_count(state) >= multiworld.advancement_goal[player]
|
||||
and state.has("Dragon Egg Shard", player, egg_shards))
|
||||
egg_shards = min(self.options.egg_shards_required.value, self.options.egg_shards_available.value)
|
||||
completion_requirements = lambda state: (location_count(state) >= self.options.advancement_goal
|
||||
and state.has("Dragon Egg Shard", player, egg_shards))
|
||||
multiworld.completion_condition[player] = lambda state: completion_requirements(state) and defeated_bosses(state)
|
||||
|
||||
# Set exclusions on hard/unreasonable/postgame
|
||||
excluded_advancements = set()
|
||||
if not multiworld.include_hard_advancements[player]:
|
||||
if not self.options.include_hard_advancements:
|
||||
excluded_advancements.update(Constants.exclusion_info["hard"])
|
||||
if not multiworld.include_unreasonable_advancements[player]:
|
||||
if not self.options.include_unreasonable_advancements:
|
||||
excluded_advancements.update(Constants.exclusion_info["unreasonable"])
|
||||
if not multiworld.include_postgame_advancements[player]:
|
||||
if not self.options.include_postgame_advancements:
|
||||
excluded_advancements.update(postgame_advancements)
|
||||
exclusion_rules(multiworld, player, excluded_advancements)
|
||||
|
||||
@@ -1,17 +1,19 @@
|
||||
from worlds.AutoWorld import World
|
||||
|
||||
from . import Constants
|
||||
from typing import TYPE_CHECKING
|
||||
if TYPE_CHECKING:
|
||||
from . import MinecraftWorld
|
||||
|
||||
def shuffle_structures(mc_world: World) -> None:
|
||||
multiworld = mc_world.multiworld
|
||||
player = mc_world.player
|
||||
|
||||
def shuffle_structures(self: "MinecraftWorld") -> None:
|
||||
multiworld = self.multiworld
|
||||
player = self.player
|
||||
|
||||
default_connections = Constants.region_info["default_connections"]
|
||||
illegal_connections = Constants.region_info["illegal_connections"]
|
||||
|
||||
# Get all unpaired exits and all regions without entrances (except the Menu)
|
||||
# This function is destructive on these lists.
|
||||
exits = [exit.name for r in multiworld.regions if r.player == player for exit in r.exits if exit.connected_region == None]
|
||||
exits = [exit.name for r in multiworld.regions if r.player == player for exit in r.exits if exit.connected_region is None]
|
||||
structs = [r.name for r in multiworld.regions if r.player == player and r.entrances == [] and r.name != 'Menu']
|
||||
exits_spoiler = exits[:] # copy the original order for the spoiler log
|
||||
|
||||
@@ -26,19 +28,19 @@ def shuffle_structures(mc_world: World) -> None:
|
||||
raise Exception(f"Invalid connection: {exit} => {struct} for player {player} ({multiworld.player_name[player]})")
|
||||
|
||||
# Connect plando structures first
|
||||
if multiworld.plando_connections[player]:
|
||||
for conn in multiworld.plando_connections[player]:
|
||||
if self.options.plando_connections:
|
||||
for conn in self.plando_connections:
|
||||
set_pair(conn.entrance, conn.exit)
|
||||
|
||||
# The algorithm tries to place the most restrictive structures first. This algorithm always works on the
|
||||
# relatively small set of restrictions here, but does not work on all possible inputs with valid configurations.
|
||||
if multiworld.shuffle_structures[player]:
|
||||
if self.options.shuffle_structures:
|
||||
structs.sort(reverse=True, key=lambda s: len(illegal_connections.get(s, [])))
|
||||
for struct in structs[:]:
|
||||
try:
|
||||
exit = multiworld.random.choice([e for e in exits if e not in illegal_connections.get(struct, [])])
|
||||
exit = self.random.choice([e for e in exits if e not in illegal_connections.get(struct, [])])
|
||||
except IndexError:
|
||||
raise Exception(f"No valid structure placements remaining for player {player} ({multiworld.player_name[player]})")
|
||||
raise Exception(f"No valid structure placements remaining for player {player} ({self.player_name})")
|
||||
set_pair(exit, struct)
|
||||
else: # write remaining default connections
|
||||
for (exit, struct) in default_connections:
|
||||
@@ -49,9 +51,9 @@ def shuffle_structures(mc_world: World) -> None:
|
||||
try:
|
||||
assert len(exits) == len(structs) == 0
|
||||
except AssertionError:
|
||||
raise Exception(f"Failed to connect all Minecraft structures for player {player} ({multiworld.player_name[player]})")
|
||||
raise Exception(f"Failed to connect all Minecraft structures for player {player} ({self.player_name})")
|
||||
|
||||
for exit in exits_spoiler:
|
||||
multiworld.get_entrance(exit, player).connect(multiworld.get_region(pairs[exit], player))
|
||||
if multiworld.shuffle_structures[player] or multiworld.plando_connections[player]:
|
||||
if self.options.shuffle_structures or self.options.plando_connections:
|
||||
multiworld.spoiler.set_entrance(exit, pairs[exit], 'entrance', player)
|
||||
|
||||
@@ -9,7 +9,7 @@ from BaseClasses import Region, Entrance, Item, Tutorial, ItemClassification, Lo
|
||||
from worlds.AutoWorld import World, WebWorld
|
||||
|
||||
from . import Constants
|
||||
from .Options import minecraft_options
|
||||
from .Options import MinecraftOptions
|
||||
from .Structures import shuffle_structures
|
||||
from .ItemPool import build_item_pool, get_junk_item_names
|
||||
from .Rules import set_rules
|
||||
@@ -83,8 +83,9 @@ class MinecraftWorld(World):
|
||||
structures, and materials to create a portal to another world. Defeat the Ender Dragon, and claim
|
||||
victory!
|
||||
"""
|
||||
game: str = "Minecraft"
|
||||
option_definitions = minecraft_options
|
||||
game = "Minecraft"
|
||||
options_dataclass = MinecraftOptions
|
||||
options: MinecraftOptions
|
||||
settings: typing.ClassVar[MinecraftSettings]
|
||||
topology_present = True
|
||||
web = MinecraftWebWorld()
|
||||
@@ -95,20 +96,20 @@ class MinecraftWorld(World):
|
||||
def _get_mc_data(self) -> Dict[str, Any]:
|
||||
exits = [connection[0] for connection in Constants.region_info["default_connections"]]
|
||||
return {
|
||||
'world_seed': self.multiworld.per_slot_randoms[self.player].getrandbits(32),
|
||||
'world_seed': self.random.getrandbits(32),
|
||||
'seed_name': self.multiworld.seed_name,
|
||||
'player_name': self.multiworld.get_player_name(self.player),
|
||||
'player_name': self.player_name,
|
||||
'player_id': self.player,
|
||||
'client_version': client_version,
|
||||
'structures': {exit: self.multiworld.get_entrance(exit, self.player).connected_region.name for exit in exits},
|
||||
'advancement_goal': self.multiworld.advancement_goal[self.player].value,
|
||||
'egg_shards_required': min(self.multiworld.egg_shards_required[self.player].value,
|
||||
self.multiworld.egg_shards_available[self.player].value),
|
||||
'egg_shards_available': self.multiworld.egg_shards_available[self.player].value,
|
||||
'required_bosses': self.multiworld.required_bosses[self.player].current_key,
|
||||
'MC35': bool(self.multiworld.send_defeated_mobs[self.player].value),
|
||||
'death_link': bool(self.multiworld.death_link[self.player].value),
|
||||
'starting_items': str(self.multiworld.starting_items[self.player].value),
|
||||
'advancement_goal': self.options.advancement_goal.value,
|
||||
'egg_shards_required': min(self.options.egg_shards_required.value,
|
||||
self.options.egg_shards_available.value),
|
||||
'egg_shards_available': self.options.egg_shards_available.value,
|
||||
'required_bosses': self.options.required_bosses.current_key,
|
||||
'MC35': bool(self.options.send_defeated_mobs.value),
|
||||
'death_link': bool(self.options.death_link.value),
|
||||
'starting_items': json.dumps(self.options.starting_items.value),
|
||||
'race': self.multiworld.is_race,
|
||||
}
|
||||
|
||||
@@ -129,7 +130,7 @@ class MinecraftWorld(World):
|
||||
loc.place_locked_item(self.create_event_item(event_name))
|
||||
region.locations.append(loc)
|
||||
|
||||
def create_event_item(self, name: str) -> None:
|
||||
def create_event_item(self, name: str) -> Item:
|
||||
item = self.create_item(name)
|
||||
item.classification = ItemClassification.progression
|
||||
return item
|
||||
@@ -176,15 +177,10 @@ class MinecraftWorld(World):
|
||||
f.write(b64encode(bytes(json.dumps(data), 'utf-8')))
|
||||
|
||||
def fill_slot_data(self) -> dict:
|
||||
slot_data = self._get_mc_data()
|
||||
for option_name in minecraft_options:
|
||||
option = getattr(self.multiworld, option_name)[self.player]
|
||||
if slot_data.get(option_name, None) is None and type(option.value) in {str, int}:
|
||||
slot_data[option_name] = int(option.value)
|
||||
return slot_data
|
||||
return self._get_mc_data()
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
return get_junk_item_names(self.multiworld.random, 1)[0]
|
||||
return get_junk_item_names(self.random, 1)[0]
|
||||
|
||||
|
||||
class MinecraftLocation(Location):
|
||||
|
||||
@@ -1,19 +1,19 @@
|
||||
from . import MCTestBase
|
||||
from ..Constants import region_info
|
||||
from ..Options import minecraft_options
|
||||
from .. import Options
|
||||
|
||||
from BaseClasses import ItemClassification
|
||||
|
||||
class AdvancementTestBase(MCTestBase):
|
||||
options = {
|
||||
"advancement_goal": minecraft_options["advancement_goal"].range_end
|
||||
"advancement_goal": Options.AdvancementGoal.range_end
|
||||
}
|
||||
# beatability test implicit
|
||||
|
||||
class ShardTestBase(MCTestBase):
|
||||
options = {
|
||||
"egg_shards_required": minecraft_options["egg_shards_required"].range_end,
|
||||
"egg_shards_available": minecraft_options["egg_shards_available"].range_end
|
||||
"egg_shards_required": Options.EggShardsRequired.range_end,
|
||||
"egg_shards_available": Options.EggShardsAvailable.range_end
|
||||
}
|
||||
|
||||
# check that itempool is not overfilled with shards
|
||||
@@ -29,7 +29,7 @@ class CompassTestBase(MCTestBase):
|
||||
|
||||
class NoBeeTestBase(MCTestBase):
|
||||
options = {
|
||||
"bee_traps": 0
|
||||
"bee_traps": Options.BeeTraps.range_start
|
||||
}
|
||||
|
||||
# With no bees, there are no traps in the pool
|
||||
@@ -40,7 +40,7 @@ class NoBeeTestBase(MCTestBase):
|
||||
|
||||
class AllBeeTestBase(MCTestBase):
|
||||
options = {
|
||||
"bee_traps": 100
|
||||
"bee_traps": Options.BeeTraps.range_end
|
||||
}
|
||||
|
||||
# With max bees, there are no filler items, only bee traps
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
from test.TestBase import TestBase, WorldTestBase
|
||||
from .. import MinecraftWorld
|
||||
from test.bases import TestBase, WorldTestBase
|
||||
from .. import MinecraftWorld, MinecraftOptions
|
||||
|
||||
|
||||
class MCTestBase(WorldTestBase, TestBase):
|
||||
|
||||
290
worlds/mm2/__init__.py
Normal file
290
worlds/mm2/__init__.py
Normal file
@@ -0,0 +1,290 @@
|
||||
import hashlib
|
||||
import logging
|
||||
from copy import deepcopy
|
||||
from typing import Dict, Any, TYPE_CHECKING, Optional, Sequence, Tuple, ClassVar, List
|
||||
|
||||
from BaseClasses import Tutorial, ItemClassification, MultiWorld, Item, Location
|
||||
from worlds.AutoWorld import World, WebWorld
|
||||
from .names import (dr_wily, heat_man_stage, air_man_stage, wood_man_stage, bubble_man_stage, quick_man_stage,
|
||||
flash_man_stage, metal_man_stage, crash_man_stage)
|
||||
from .items import (item_table, item_names, MM2Item, filler_item_weights, robot_master_weapon_table,
|
||||
stage_access_table, item_item_table, lookup_item_to_id)
|
||||
from .locations import (MM2Location, mm2_regions, MM2Region, energy_pickups, etank_1ups, lookup_location_to_id,
|
||||
location_groups)
|
||||
from .rom import patch_rom, MM2ProcedurePatch, MM2LCHASH, PROTEUSHASH, MM2VCHASH, MM2NESHASH
|
||||
from .options import MM2Options, Consumables
|
||||
from .client import MegaMan2Client
|
||||
from .rules import set_rules, weapon_damage, robot_masters, weapons_to_name, minimum_weakness_requirement
|
||||
import os
|
||||
import threading
|
||||
import base64
|
||||
import settings
|
||||
logger = logging.getLogger("Mega Man 2")
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from BaseClasses import CollectionState
|
||||
|
||||
|
||||
class MM2Settings(settings.Group):
|
||||
class RomFile(settings.UserFilePath):
|
||||
"""File name of the MM2 EN rom"""
|
||||
description = "Mega Man 2 ROM File"
|
||||
copy_to: Optional[str] = "Mega Man 2 (USA).nes"
|
||||
md5s = [MM2NESHASH, MM2VCHASH, MM2LCHASH, PROTEUSHASH]
|
||||
|
||||
def browse(self: settings.T,
|
||||
filetypes: Optional[Sequence[Tuple[str, Sequence[str]]]] = None,
|
||||
**kwargs: Any) -> Optional[settings.T]:
|
||||
if not filetypes:
|
||||
file_types = [("NES", [".nes"]), ("Program", [".exe"])] # LC1 is only a windows executable, no linux
|
||||
return super().browse(file_types, **kwargs)
|
||||
else:
|
||||
return super().browse(filetypes, **kwargs)
|
||||
|
||||
@classmethod
|
||||
def validate(cls, path: str) -> None:
|
||||
"""Try to open and validate file against hashes"""
|
||||
with open(path, "rb", buffering=0) as f:
|
||||
try:
|
||||
f.seek(0)
|
||||
if f.read(4) == b"NES\x1A":
|
||||
f.seek(16)
|
||||
else:
|
||||
f.seek(0)
|
||||
cls._validate_stream_hashes(f)
|
||||
base_rom_bytes = f.read()
|
||||
basemd5 = hashlib.md5()
|
||||
basemd5.update(base_rom_bytes)
|
||||
if basemd5.hexdigest() == PROTEUSHASH:
|
||||
# we need special behavior here
|
||||
cls.copy_to = None
|
||||
except ValueError:
|
||||
raise ValueError(f"File hash does not match for {path}")
|
||||
|
||||
rom_file: RomFile = RomFile(RomFile.copy_to)
|
||||
|
||||
|
||||
class MM2WebWorld(WebWorld):
|
||||
theme = "partyTime"
|
||||
tutorials = [
|
||||
|
||||
Tutorial(
|
||||
"Multiworld Setup Guide",
|
||||
"A guide to setting up the Mega Man 2 randomizer connected to an Archipelago Multiworld.",
|
||||
"English",
|
||||
"setup_en.md",
|
||||
"setup/en",
|
||||
["Silvris"]
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
class MM2World(World):
|
||||
"""
|
||||
In the year 200X, following his prior defeat by Mega Man, the evil Dr. Wily has returned to take over the world with
|
||||
his own group of Robot Masters. Mega Man once again sets out to defeat the eight Robot Masters and stop Dr. Wily.
|
||||
|
||||
"""
|
||||
|
||||
game = "Mega Man 2"
|
||||
settings: ClassVar[MM2Settings]
|
||||
options_dataclass = MM2Options
|
||||
options: MM2Options
|
||||
item_name_to_id = lookup_item_to_id
|
||||
location_name_to_id = lookup_location_to_id
|
||||
item_name_groups = item_names
|
||||
location_name_groups = location_groups
|
||||
web = MM2WebWorld()
|
||||
rom_name: bytearray
|
||||
world_version: Tuple[int, int, int] = (0, 3, 1)
|
||||
wily_5_weapons: Dict[int, List[int]]
|
||||
|
||||
def __init__(self, world: MultiWorld, player: int):
|
||||
self.rom_name = bytearray()
|
||||
self.rom_name_available_event = threading.Event()
|
||||
super().__init__(world, player)
|
||||
self.weapon_damage = deepcopy(weapon_damage)
|
||||
self.wily_5_weapons = {}
|
||||
|
||||
def create_regions(self) -> None:
|
||||
menu = MM2Region("Menu", self.player, self.multiworld)
|
||||
self.multiworld.regions.append(menu)
|
||||
for region in mm2_regions:
|
||||
stage = MM2Region(region, self.player, self.multiworld)
|
||||
required_items = mm2_regions[region][0]
|
||||
locations = mm2_regions[region][1]
|
||||
prev_stage = mm2_regions[region][2]
|
||||
if prev_stage is None:
|
||||
menu.connect(stage, f"To {region}",
|
||||
lambda state, items=required_items: state.has_all(items, self.player))
|
||||
else:
|
||||
old_stage = self.get_region(prev_stage)
|
||||
old_stage.connect(stage, f"To {region}",
|
||||
lambda state, items=required_items: state.has_all(items, self.player))
|
||||
stage.add_locations(locations, MM2Location)
|
||||
for location in stage.get_locations():
|
||||
if location.address is None and location.name != dr_wily:
|
||||
location.place_locked_item(MM2Item(location.name, ItemClassification.progression,
|
||||
None, self.player))
|
||||
if region in etank_1ups and self.options.consumables in (Consumables.option_1up_etank,
|
||||
Consumables.option_all):
|
||||
stage.add_locations(etank_1ups[region], MM2Location)
|
||||
if region in energy_pickups and self.options.consumables in (Consumables.option_weapon_health,
|
||||
Consumables.option_all):
|
||||
stage.add_locations(energy_pickups[region], MM2Location)
|
||||
self.multiworld.regions.append(stage)
|
||||
|
||||
def create_item(self, name: str) -> MM2Item:
|
||||
item = item_table[name]
|
||||
classification = ItemClassification.filler
|
||||
if item.progression:
|
||||
classification = ItemClassification.progression_skip_balancing \
|
||||
if item.skip_balancing else ItemClassification.progression
|
||||
if item.useful:
|
||||
classification |= ItemClassification.useful
|
||||
return MM2Item(name, classification, item.code, self.player)
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
return self.random.choices(list(filler_item_weights.keys()),
|
||||
weights=list(filler_item_weights.values()))[0]
|
||||
|
||||
def create_items(self) -> None:
|
||||
itempool = []
|
||||
# grab first robot master
|
||||
robot_master = self.item_id_to_name[0x880101 + self.options.starting_robot_master.value]
|
||||
self.multiworld.push_precollected(self.create_item(robot_master))
|
||||
itempool.extend([self.create_item(name) for name in stage_access_table.keys()
|
||||
if name != robot_master])
|
||||
itempool.extend([self.create_item(name) for name in robot_master_weapon_table.keys()])
|
||||
itempool.extend([self.create_item(name) for name in item_item_table.keys()])
|
||||
total_checks = 24
|
||||
if self.options.consumables in (Consumables.option_1up_etank,
|
||||
Consumables.option_all):
|
||||
total_checks += 20
|
||||
if self.options.consumables in (Consumables.option_weapon_health,
|
||||
Consumables.option_all):
|
||||
total_checks += 27
|
||||
remaining = total_checks - len(itempool)
|
||||
itempool.extend([self.create_item(name)
|
||||
for name in self.random.choices(list(filler_item_weights.keys()),
|
||||
weights=list(filler_item_weights.values()),
|
||||
k=remaining)])
|
||||
self.multiworld.itempool += itempool
|
||||
|
||||
set_rules = set_rules
|
||||
|
||||
def generate_early(self) -> None:
|
||||
if (not self.options.yoku_jumps
|
||||
and self.options.starting_robot_master == "heat_man") or \
|
||||
(not self.options.enable_lasers
|
||||
and self.options.starting_robot_master == "quick_man"):
|
||||
robot_master_pool = [1, 2, 3, 5, 6, 7, ]
|
||||
if self.options.yoku_jumps:
|
||||
robot_master_pool.append(0)
|
||||
if self.options.enable_lasers:
|
||||
robot_master_pool.append(4)
|
||||
self.options.starting_robot_master.value = self.random.choice(robot_master_pool)
|
||||
logger.warning(
|
||||
f"Mega Man 2 ({self.player_name}): "
|
||||
f"Incompatible starting Robot Master, changing to "
|
||||
f"{self.options.starting_robot_master.current_key.replace('_', ' ').title()}")
|
||||
|
||||
def generate_basic(self) -> None:
|
||||
goal_location = self.get_location(dr_wily)
|
||||
goal_location.place_locked_item(MM2Item("Victory", ItemClassification.progression, None, self.player))
|
||||
self.multiworld.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
|
||||
|
||||
def fill_hook(self,
|
||||
progitempool: List["Item"],
|
||||
usefulitempool: List["Item"],
|
||||
filleritempool: List["Item"],
|
||||
fill_locations: List["Location"]) -> None:
|
||||
# on a solo gen, fill can try to force Wily into sphere 2, but for most generations this is impossible
|
||||
# since MM2 can have a 2 item sphere 1, and 3 items are required for Wily
|
||||
if self.multiworld.players > 1:
|
||||
return # Don't need to change anything on a multi gen, fill should be able to solve it with a 4 sphere 1
|
||||
rbm_to_item = {
|
||||
0: heat_man_stage,
|
||||
1: air_man_stage,
|
||||
2: wood_man_stage,
|
||||
3: bubble_man_stage,
|
||||
4: quick_man_stage,
|
||||
5: flash_man_stage,
|
||||
6: metal_man_stage,
|
||||
7: crash_man_stage
|
||||
}
|
||||
affected_rbm = [2, 3] # Wood and Bubble will always have this happen
|
||||
possible_rbm = [1, 5] # Air and Flash are always valid targets, due to Item 2/3 receive
|
||||
if self.options.consumables:
|
||||
possible_rbm.append(6) # Metal has 3 consumables
|
||||
possible_rbm.append(7) # Crash has 3 consumables
|
||||
if self.options.enable_lasers:
|
||||
possible_rbm.append(4) # Quick has a lot of consumables, but needs logical time stopper if not enabled
|
||||
else:
|
||||
affected_rbm.extend([6, 7]) # only two checks on non consumables
|
||||
if self.options.yoku_jumps:
|
||||
possible_rbm.append(0) # Heat has 3 locations always, but might need 2 items logically
|
||||
if self.options.starting_robot_master.value in affected_rbm:
|
||||
rbm_names = list(map(lambda s: rbm_to_item[s], possible_rbm))
|
||||
valid_second = [item for item in progitempool
|
||||
if item.name in rbm_names
|
||||
and item.player == self.player]
|
||||
placed_item = self.random.choice(valid_second)
|
||||
rbm_defeated = (f"{robot_masters[self.options.starting_robot_master.value].replace(' Defeated', '')}"
|
||||
f" - Defeated")
|
||||
rbm_location = self.get_location(rbm_defeated)
|
||||
rbm_location.place_locked_item(placed_item)
|
||||
progitempool.remove(placed_item)
|
||||
fill_locations.remove(rbm_location)
|
||||
target_rbm = (placed_item.code & 0xF) - 1
|
||||
if self.options.strict_weakness or (self.options.random_weakness
|
||||
and not (self.weapon_damage[0][target_rbm] > 0)):
|
||||
# we need to find a weakness for this boss
|
||||
weaknesses = [weapon for weapon in range(1, 9)
|
||||
if self.weapon_damage[weapon][target_rbm] >= minimum_weakness_requirement[weapon]]
|
||||
weapons = list(map(lambda s: weapons_to_name[s], weaknesses))
|
||||
valid_weapons = [item for item in progitempool
|
||||
if item.name in weapons
|
||||
and item.player == self.player]
|
||||
placed_weapon = self.random.choice(valid_weapons)
|
||||
weapon_name = next(name for name, idx in lookup_location_to_id.items()
|
||||
if idx == 0x880101 + self.options.starting_robot_master.value)
|
||||
weapon_location = self.get_location(weapon_name)
|
||||
weapon_location.place_locked_item(placed_weapon)
|
||||
progitempool.remove(placed_weapon)
|
||||
fill_locations.remove(weapon_location)
|
||||
|
||||
def generate_output(self, output_directory: str) -> None:
|
||||
try:
|
||||
patch = MM2ProcedurePatch(player=self.player, player_name=self.player_name)
|
||||
patch_rom(self, patch)
|
||||
|
||||
self.rom_name = patch.name
|
||||
|
||||
patch.write(os.path.join(output_directory,
|
||||
f"{self.multiworld.get_out_file_name_base(self.player)}{patch.patch_file_ending}"))
|
||||
except Exception:
|
||||
raise
|
||||
finally:
|
||||
self.rom_name_available_event.set() # make sure threading continues and errors are collected
|
||||
|
||||
def fill_slot_data(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"death_link": self.options.death_link.value,
|
||||
"weapon_damage": self.weapon_damage,
|
||||
"wily_5_weapons": self.wily_5_weapons,
|
||||
}
|
||||
|
||||
def interpret_slot_data(self, slot_data: Dict[str, Any]) -> Dict[str, Any]:
|
||||
local_weapon = {int(key): value for key, value in slot_data["weapon_damage"].items()}
|
||||
local_wily = {int(key): value for key, value in slot_data["wily_5_weapons"].items()}
|
||||
return {"weapon_damage": local_weapon, "wily_5_weapons": local_wily}
|
||||
|
||||
def modify_multidata(self, multidata: Dict[str, Any]) -> None:
|
||||
# wait for self.rom_name to be available.
|
||||
self.rom_name_available_event.wait()
|
||||
rom_name = getattr(self, "rom_name", None)
|
||||
# we skip in case of error, so that the original error in the output thread is the one that gets raised
|
||||
if rom_name:
|
||||
new_name = base64.b64encode(bytes(self.rom_name)).decode()
|
||||
multidata["connect_names"][new_name] = multidata["connect_names"][self.player_name]
|
||||
562
worlds/mm2/client.py
Normal file
562
worlds/mm2/client.py
Normal file
@@ -0,0 +1,562 @@
|
||||
import logging
|
||||
import time
|
||||
from enum import IntEnum
|
||||
from base64 import b64encode
|
||||
from typing import TYPE_CHECKING, Dict, Tuple, List, Optional, Any
|
||||
from NetUtils import ClientStatus, color, NetworkItem
|
||||
from worlds._bizhawk.client import BizHawkClient
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from worlds._bizhawk.context import BizHawkClientContext, BizHawkClientCommandProcessor
|
||||
|
||||
nes_logger = logging.getLogger("NES")
|
||||
logger = logging.getLogger("Client")
|
||||
|
||||
MM2_ROBOT_MASTERS_UNLOCKED = 0x8A
|
||||
MM2_ROBOT_MASTERS_DEFEATED = 0x8B
|
||||
MM2_ITEMS_ACQUIRED = 0x8C
|
||||
MM2_LAST_WILY = 0x8D
|
||||
MM2_RECEIVED_ITEMS = 0x8E
|
||||
MM2_DEATHLINK = 0x8F
|
||||
MM2_ENERGYLINK = 0x90
|
||||
MM2_RBM_STROBE = 0x91
|
||||
MM2_WEAPONS_UNLOCKED = 0x9A
|
||||
MM2_ITEMS_UNLOCKED = 0x9B
|
||||
MM2_WEAPON_ENERGY = 0x9C
|
||||
MM2_E_TANKS = 0xA7
|
||||
MM2_LIVES = 0xA8
|
||||
MM2_DIFFICULTY = 0xCB
|
||||
MM2_HEALTH = 0x6C0
|
||||
MM2_COMPLETED_STAGES = 0x770
|
||||
MM2_CONSUMABLES = 0x780
|
||||
|
||||
MM2_SFX_QUEUE = 0x580
|
||||
MM2_SFX_STROBE = 0x66
|
||||
|
||||
MM2_CONSUMABLE_TABLE: Dict[int, Tuple[int, int]] = {
|
||||
# Item: (byte offset, bit mask)
|
||||
0x880201: (0, 8),
|
||||
0x880202: (16, 1),
|
||||
0x880203: (16, 2),
|
||||
0x880204: (16, 4),
|
||||
0x880205: (16, 8),
|
||||
0x880206: (16, 16),
|
||||
0x880207: (16, 32),
|
||||
0x880208: (16, 64),
|
||||
0x880209: (16, 128),
|
||||
0x88020A: (20, 1),
|
||||
0x88020B: (20, 4),
|
||||
0x88020C: (20, 64),
|
||||
0x88020D: (21, 1),
|
||||
0x88020E: (21, 2),
|
||||
0x88020F: (21, 4),
|
||||
0x880210: (24, 1),
|
||||
0x880211: (24, 2),
|
||||
0x880212: (24, 4),
|
||||
0x880213: (28, 1),
|
||||
0x880214: (28, 2),
|
||||
0x880215: (28, 4),
|
||||
0x880216: (33, 4),
|
||||
0x880217: (33, 8),
|
||||
0x880218: (37, 8),
|
||||
0x880219: (37, 16),
|
||||
0x88021A: (38, 1),
|
||||
0x88021B: (38, 2),
|
||||
0x880227: (38, 4),
|
||||
0x880228: (38, 32),
|
||||
0x880229: (38, 128),
|
||||
0x88022A: (39, 4),
|
||||
0x88022B: (39, 2),
|
||||
0x88022C: (39, 1),
|
||||
0x88022D: (38, 64),
|
||||
0x88022E: (38, 16),
|
||||
0x88022F: (38, 8),
|
||||
0x88021C: (39, 32),
|
||||
0x88021D: (39, 64),
|
||||
0x88021E: (39, 128),
|
||||
0x88021F: (41, 16),
|
||||
0x880220: (42, 2),
|
||||
0x880221: (42, 4),
|
||||
0x880222: (42, 8),
|
||||
0x880223: (46, 1),
|
||||
0x880224: (46, 2),
|
||||
0x880225: (46, 4),
|
||||
0x880226: (46, 8),
|
||||
}
|
||||
|
||||
|
||||
class MM2EnergyLinkType(IntEnum):
|
||||
Life = 0
|
||||
AtomicFire = 1
|
||||
AirShooter = 2
|
||||
LeafShield = 3
|
||||
BubbleLead = 4
|
||||
QuickBoomerang = 5
|
||||
TimeStopper = 6
|
||||
MetalBlade = 7
|
||||
CrashBomber = 8
|
||||
Item1 = 9
|
||||
Item2 = 10
|
||||
Item3 = 11
|
||||
OneUP = 12
|
||||
|
||||
|
||||
request_to_name: Dict[str, str] = {
|
||||
"HP": "health",
|
||||
"AF": "Atomic Fire energy",
|
||||
"AS": "Air Shooter energy",
|
||||
"LS": "Leaf Shield energy",
|
||||
"BL": "Bubble Lead energy",
|
||||
"QB": "Quick Boomerang energy",
|
||||
"TS": "Time Stopper energy",
|
||||
"MB": "Metal Blade energy",
|
||||
"CB": "Crash Bomber energy",
|
||||
"I1": "Item 1 energy",
|
||||
"I2": "Item 2 energy",
|
||||
"I3": "Item 3 energy",
|
||||
"1U": "lives"
|
||||
}
|
||||
|
||||
HP_EXCHANGE_RATE = 500000000
|
||||
WEAPON_EXCHANGE_RATE = 250000000
|
||||
ONEUP_EXCHANGE_RATE = 14000000000
|
||||
|
||||
|
||||
def cmd_pool(self: "BizHawkClientCommandProcessor") -> None:
|
||||
"""Check the current pool of EnergyLink, and requestable refills from it."""
|
||||
if self.ctx.game != "Mega Man 2":
|
||||
logger.warning("This command can only be used when playing Mega Man 2.")
|
||||
return
|
||||
if not self.ctx.server or not self.ctx.slot:
|
||||
logger.warning("You must be connected to a server to use this command.")
|
||||
return
|
||||
energylink = self.ctx.stored_data.get(f"EnergyLink{self.ctx.team}", 0)
|
||||
health_points = energylink // HP_EXCHANGE_RATE
|
||||
weapon_points = energylink // WEAPON_EXCHANGE_RATE
|
||||
lives = energylink // ONEUP_EXCHANGE_RATE
|
||||
logger.info(f"Healing available: {health_points}\n"
|
||||
f"Weapon refill available: {weapon_points}\n"
|
||||
f"Lives available: {lives}")
|
||||
|
||||
|
||||
def cmd_request(self: "BizHawkClientCommandProcessor", amount: str, target: str) -> None:
|
||||
from worlds._bizhawk.context import BizHawkClientContext
|
||||
"""Request a refill from EnergyLink."""
|
||||
if self.ctx.game != "Mega Man 2":
|
||||
logger.warning("This command can only be used when playing Mega Man 2.")
|
||||
return
|
||||
if not self.ctx.server or not self.ctx.slot:
|
||||
logger.warning("You must be connected to a server to use this command.")
|
||||
return
|
||||
valid_targets: Dict[str, MM2EnergyLinkType] = {
|
||||
"HP": MM2EnergyLinkType.Life,
|
||||
"AF": MM2EnergyLinkType.AtomicFire,
|
||||
"AS": MM2EnergyLinkType.AirShooter,
|
||||
"LS": MM2EnergyLinkType.LeafShield,
|
||||
"BL": MM2EnergyLinkType.BubbleLead,
|
||||
"QB": MM2EnergyLinkType.QuickBoomerang,
|
||||
"TS": MM2EnergyLinkType.TimeStopper,
|
||||
"MB": MM2EnergyLinkType.MetalBlade,
|
||||
"CB": MM2EnergyLinkType.CrashBomber,
|
||||
"I1": MM2EnergyLinkType.Item1,
|
||||
"I2": MM2EnergyLinkType.Item2,
|
||||
"I3": MM2EnergyLinkType.Item3,
|
||||
"1U": MM2EnergyLinkType.OneUP
|
||||
}
|
||||
if target.upper() not in valid_targets:
|
||||
logger.warning(f"Unrecognized target {target.upper()}. Available targets: {', '.join(valid_targets.keys())}")
|
||||
return
|
||||
ctx = self.ctx
|
||||
assert isinstance(ctx, BizHawkClientContext)
|
||||
client = ctx.client_handler
|
||||
assert isinstance(client, MegaMan2Client)
|
||||
client.refill_queue.append((valid_targets[target.upper()], int(amount)))
|
||||
logger.info(f"Restoring {amount} {request_to_name[target.upper()]}.")
|
||||
|
||||
|
||||
def cmd_autoheal(self) -> None:
|
||||
"""Enable auto heal from EnergyLink."""
|
||||
if self.ctx.game != "Mega Man 2":
|
||||
logger.warning("This command can only be used when playing Mega Man 2.")
|
||||
return
|
||||
if not self.ctx.server or not self.ctx.slot:
|
||||
logger.warning("You must be connected to a server to use this command.")
|
||||
return
|
||||
else:
|
||||
assert isinstance(self.ctx.client_handler, MegaMan2Client)
|
||||
if self.ctx.client_handler.auto_heal:
|
||||
self.ctx.client_handler.auto_heal = False
|
||||
logger.info(f"Auto healing disabled.")
|
||||
else:
|
||||
self.ctx.client_handler.auto_heal = True
|
||||
logger.info(f"Auto healing enabled.")
|
||||
|
||||
|
||||
def get_sfx_writes(sfx: int) -> Tuple[Tuple[int, bytes, str], ...]:
|
||||
return (MM2_SFX_QUEUE, sfx.to_bytes(1, 'little'), "RAM"), (MM2_SFX_STROBE, 0x01.to_bytes(1, "little"), "RAM")
|
||||
|
||||
|
||||
class MegaMan2Client(BizHawkClient):
|
||||
game = "Mega Man 2"
|
||||
system = "NES"
|
||||
patch_suffix = ".apmm2"
|
||||
item_queue: List[NetworkItem] = []
|
||||
pending_death_link: bool = False
|
||||
# default to true, as we don't want to send a deathlink until Mega Man's HP is initialized once
|
||||
sending_death_link: bool = True
|
||||
death_link: bool = False
|
||||
energy_link: bool = False
|
||||
rom: Optional[bytes] = None
|
||||
weapon_energy: int = 0
|
||||
health_energy: int = 0
|
||||
auto_heal: bool = False
|
||||
refill_queue: List[Tuple[MM2EnergyLinkType, int]] = []
|
||||
last_wily: Optional[int] = None # default to wily 1
|
||||
|
||||
async def validate_rom(self, ctx: "BizHawkClientContext") -> bool:
|
||||
from worlds._bizhawk import RequestFailedError, read
|
||||
from . import MM2World
|
||||
|
||||
try:
|
||||
game_name, version = (await read(ctx.bizhawk_ctx, [(0x3FFB0, 21, "PRG ROM"),
|
||||
(0x3FFC8, 3, "PRG ROM")]))
|
||||
if game_name[:3] != b"MM2" or version != bytes(MM2World.world_version):
|
||||
if game_name[:3] == b"MM2":
|
||||
# I think this is an easier check than the other?
|
||||
older_version = "0.2.1" if version == b"\xFF\xFF\xFF" else f"{version[0]}.{version[1]}.{version[2]}"
|
||||
logger.warning(f"This Mega Man 2 patch was generated for an different version of the apworld. "
|
||||
f"Please use that version to connect instead.\n"
|
||||
f"Patch version: ({older_version})\n"
|
||||
f"Client version: ({'.'.join([str(i) for i in MM2World.world_version])})")
|
||||
if "pool" in ctx.command_processor.commands:
|
||||
ctx.command_processor.commands.pop("pool")
|
||||
if "request" in ctx.command_processor.commands:
|
||||
ctx.command_processor.commands.pop("request")
|
||||
if "autoheal" in ctx.command_processor.commands:
|
||||
ctx.command_processor.commands.pop("autoheal")
|
||||
return False
|
||||
except UnicodeDecodeError:
|
||||
return False
|
||||
except RequestFailedError:
|
||||
return False # Should verify on the next pass
|
||||
|
||||
ctx.game = self.game
|
||||
self.rom = game_name
|
||||
ctx.items_handling = 0b111
|
||||
ctx.want_slot_data = False
|
||||
deathlink = (await read(ctx.bizhawk_ctx, [(0x3FFC5, 1, "PRG ROM")]))[0][0]
|
||||
if deathlink & 0x01:
|
||||
self.death_link = True
|
||||
if deathlink & 0x02:
|
||||
self.energy_link = True
|
||||
|
||||
if self.energy_link:
|
||||
if "pool" not in ctx.command_processor.commands:
|
||||
ctx.command_processor.commands["pool"] = cmd_pool
|
||||
if "request" not in ctx.command_processor.commands:
|
||||
ctx.command_processor.commands["request"] = cmd_request
|
||||
if "autoheal" not in ctx.command_processor.commands:
|
||||
ctx.command_processor.commands["autoheal"] = cmd_autoheal
|
||||
|
||||
return True
|
||||
|
||||
async def set_auth(self, ctx: "BizHawkClientContext") -> None:
|
||||
if self.rom:
|
||||
ctx.auth = b64encode(self.rom).decode()
|
||||
|
||||
def on_package(self, ctx: "BizHawkClientContext", cmd: str, args: Dict[str, Any]) -> None:
|
||||
if cmd == "Bounced":
|
||||
if "tags" in args:
|
||||
assert ctx.slot is not None
|
||||
if "DeathLink" in args["tags"] and args["data"]["source"] != ctx.slot_info[ctx.slot].name:
|
||||
self.on_deathlink(ctx)
|
||||
elif cmd == "Retrieved":
|
||||
if f"MM2_LAST_WILY_{ctx.team}_{ctx.slot}" in args["keys"]:
|
||||
self.last_wily = args["keys"][f"MM2_LAST_WILY_{ctx.team}_{ctx.slot}"]
|
||||
elif cmd == "Connected":
|
||||
if self.energy_link:
|
||||
ctx.set_notify(f"EnergyLink{ctx.team}")
|
||||
if ctx.ui:
|
||||
ctx.ui.enable_energy_link()
|
||||
|
||||
async def send_deathlink(self, ctx: "BizHawkClientContext") -> None:
|
||||
self.sending_death_link = True
|
||||
ctx.last_death_link = time.time()
|
||||
await ctx.send_death("Mega Man was defeated.")
|
||||
|
||||
def on_deathlink(self, ctx: "BizHawkClientContext") -> None:
|
||||
ctx.last_death_link = time.time()
|
||||
self.pending_death_link = True
|
||||
|
||||
async def game_watcher(self, ctx: "BizHawkClientContext") -> None:
|
||||
from worlds._bizhawk import read, write
|
||||
|
||||
if ctx.server is None:
|
||||
return
|
||||
|
||||
if ctx.slot is None:
|
||||
return
|
||||
|
||||
# get our relevant bytes
|
||||
robot_masters_unlocked, robot_masters_defeated, items_acquired, \
|
||||
weapons_unlocked, items_unlocked, items_received, \
|
||||
completed_stages, consumable_checks, \
|
||||
e_tanks, lives, weapon_energy, health, difficulty, death_link_status, \
|
||||
energy_link_packet, last_wily = await read(ctx.bizhawk_ctx, [
|
||||
(MM2_ROBOT_MASTERS_UNLOCKED, 1, "RAM"),
|
||||
(MM2_ROBOT_MASTERS_DEFEATED, 1, "RAM"),
|
||||
(MM2_ITEMS_ACQUIRED, 1, "RAM"),
|
||||
(MM2_WEAPONS_UNLOCKED, 1, "RAM"),
|
||||
(MM2_ITEMS_UNLOCKED, 1, "RAM"),
|
||||
(MM2_RECEIVED_ITEMS, 1, "RAM"),
|
||||
(MM2_COMPLETED_STAGES, 0xE, "RAM"),
|
||||
(MM2_CONSUMABLES, 52, "RAM"),
|
||||
(MM2_E_TANKS, 1, "RAM"),
|
||||
(MM2_LIVES, 1, "RAM"),
|
||||
(MM2_WEAPON_ENERGY, 11, "RAM"),
|
||||
(MM2_HEALTH, 1, "RAM"),
|
||||
(MM2_DIFFICULTY, 1, "RAM"),
|
||||
(MM2_DEATHLINK, 1, "RAM"),
|
||||
(MM2_ENERGYLINK, 1, "RAM"),
|
||||
(MM2_LAST_WILY, 1, "RAM"),
|
||||
])
|
||||
|
||||
if difficulty[0] not in (0, 1):
|
||||
return # Game is not initialized
|
||||
|
||||
if not ctx.finished_game and completed_stages[0xD] != 0:
|
||||
# this sets on credits fade, no real better way to do this
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "StatusUpdate",
|
||||
"status": ClientStatus.CLIENT_GOAL
|
||||
}])
|
||||
writes = []
|
||||
|
||||
# deathlink
|
||||
if self.death_link:
|
||||
await ctx.update_death_link(self.death_link)
|
||||
if self.pending_death_link:
|
||||
writes.append((MM2_DEATHLINK, bytes([0x01]), "RAM"))
|
||||
self.pending_death_link = False
|
||||
self.sending_death_link = True
|
||||
if "DeathLink" in ctx.tags and ctx.last_death_link + 1 < time.time():
|
||||
if health[0] == 0x00 and not self.sending_death_link:
|
||||
await self.send_deathlink(ctx)
|
||||
elif health[0] != 0x00 and not death_link_status[0]:
|
||||
self.sending_death_link = False
|
||||
|
||||
if self.last_wily != last_wily[0]:
|
||||
if self.last_wily is None:
|
||||
# revalidate last wily from data storage
|
||||
await ctx.send_msgs([{"cmd": "Set", "key": f"MM2_LAST_WILY_{ctx.team}_{ctx.slot}", "operations": [
|
||||
{"operation": "default", "value": 8}
|
||||
]}])
|
||||
await ctx.send_msgs([{"cmd": "Get", "keys": [f"MM2_LAST_WILY_{ctx.team}_{ctx.slot}"]}])
|
||||
elif last_wily[0] == 0:
|
||||
writes.append((MM2_LAST_WILY, self.last_wily.to_bytes(1, "little"), "RAM"))
|
||||
else:
|
||||
# correct our setting
|
||||
self.last_wily = last_wily[0]
|
||||
await ctx.send_msgs([{"cmd": "Set", "key": f"MM2_LAST_WILY_{ctx.team}_{ctx.slot}", "operations": [
|
||||
{"operation": "replace", "value": self.last_wily}
|
||||
]}])
|
||||
|
||||
# handle receiving items
|
||||
recv_amount = items_received[0]
|
||||
if recv_amount < len(ctx.items_received):
|
||||
item = ctx.items_received[recv_amount]
|
||||
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
|
||||
color(ctx.item_names.lookup_in_game(item.item), 'red', 'bold'),
|
||||
color(ctx.player_names[item.player], 'yellow'),
|
||||
ctx.location_names.lookup_in_slot(item.location, item.player), recv_amount, len(ctx.items_received)))
|
||||
|
||||
if item.item & 0x130 == 0:
|
||||
# Robot Master Weapon
|
||||
new_weapons = weapons_unlocked[0] | (1 << ((item.item & 0xF) - 1))
|
||||
writes.append((MM2_WEAPONS_UNLOCKED, new_weapons.to_bytes(1, 'little'), "RAM"))
|
||||
writes.extend(get_sfx_writes(0x21))
|
||||
elif item.item & 0x30 == 0:
|
||||
# Robot Master Stage Access
|
||||
new_stages = robot_masters_unlocked[0] & ~(1 << ((item.item & 0xF) - 1))
|
||||
writes.append((MM2_ROBOT_MASTERS_UNLOCKED, new_stages.to_bytes(1, 'little'), "RAM"))
|
||||
writes.extend(get_sfx_writes(0x3a))
|
||||
writes.append((MM2_RBM_STROBE, b"\x01", "RAM"))
|
||||
elif item.item & 0x20 == 0:
|
||||
# Items
|
||||
new_items = items_unlocked[0] | (1 << ((item.item & 0xF) - 1))
|
||||
writes.append((MM2_ITEMS_UNLOCKED, new_items.to_bytes(1, 'little'), "RAM"))
|
||||
writes.extend(get_sfx_writes(0x21))
|
||||
else:
|
||||
# append to the queue, so we handle it later
|
||||
self.item_queue.append(item)
|
||||
recv_amount += 1
|
||||
writes.append((MM2_RECEIVED_ITEMS, recv_amount.to_bytes(1, 'little'), "RAM"))
|
||||
|
||||
if energy_link_packet[0]:
|
||||
pickup = energy_link_packet[0]
|
||||
if pickup in (0x76, 0x77):
|
||||
# Health pickups
|
||||
if pickup == 0x77:
|
||||
value = 2
|
||||
else:
|
||||
value = 10
|
||||
exchange_rate = HP_EXCHANGE_RATE
|
||||
elif pickup in (0x78, 0x79):
|
||||
# Weapon Energy
|
||||
if pickup == 0x79:
|
||||
value = 2
|
||||
else:
|
||||
value = 10
|
||||
exchange_rate = WEAPON_EXCHANGE_RATE
|
||||
elif pickup == 0x7B:
|
||||
# 1-Up
|
||||
value = 1
|
||||
exchange_rate = ONEUP_EXCHANGE_RATE
|
||||
else:
|
||||
# if we managed to pickup something else, we should just fall through
|
||||
value = 0
|
||||
exchange_rate = 0
|
||||
contribution = (value * exchange_rate) >> 1
|
||||
if contribution:
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "Set", "key": f"EnergyLink{ctx.team}", "slot": ctx.slot, "operations":
|
||||
[{"operation": "add", "value": contribution},
|
||||
{"operation": "max", "value": 0}]}])
|
||||
logger.info(f"Deposited {contribution / HP_EXCHANGE_RATE} health into the pool.")
|
||||
writes.append((MM2_ENERGYLINK, 0x00.to_bytes(1, "little"), "RAM"))
|
||||
|
||||
if self.weapon_energy:
|
||||
# Weapon Energy
|
||||
# We parse the whole thing to spread it as thin as possible
|
||||
current_energy = self.weapon_energy
|
||||
weapon_energy = bytearray(weapon_energy)
|
||||
for i, weapon in zip(range(len(weapon_energy)), weapon_energy):
|
||||
if weapon < 0x1C:
|
||||
missing = 0x1C - weapon
|
||||
if missing > self.weapon_energy:
|
||||
missing = self.weapon_energy
|
||||
self.weapon_energy -= missing
|
||||
weapon_energy[i] = weapon + missing
|
||||
if not self.weapon_energy:
|
||||
writes.append((MM2_WEAPON_ENERGY, weapon_energy, "RAM"))
|
||||
break
|
||||
else:
|
||||
if current_energy != self.weapon_energy:
|
||||
writes.append((MM2_WEAPON_ENERGY, weapon_energy, "RAM"))
|
||||
|
||||
if self.health_energy or self.auto_heal:
|
||||
# Health Energy
|
||||
# We save this if the player has not taken any damage
|
||||
current_health = health[0]
|
||||
if 0 < current_health < 0x1C:
|
||||
health_diff = 0x1C - current_health
|
||||
if self.health_energy:
|
||||
if health_diff > self.health_energy:
|
||||
health_diff = self.health_energy
|
||||
self.health_energy -= health_diff
|
||||
else:
|
||||
pool = ctx.stored_data.get(f"EnergyLink{ctx.team}", 0)
|
||||
if health_diff * HP_EXCHANGE_RATE > pool:
|
||||
health_diff = int(pool // HP_EXCHANGE_RATE)
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "Set", "key": f"EnergyLink{ctx.team}", "slot": ctx.slot, "operations":
|
||||
[{"operation": "add", "value": -health_diff * HP_EXCHANGE_RATE},
|
||||
{"operation": "max", "value": 0}]}])
|
||||
current_health += health_diff
|
||||
writes.append((MM2_HEALTH, current_health.to_bytes(1, 'little'), "RAM"))
|
||||
|
||||
if self.refill_queue:
|
||||
refill_type, refill_amount = self.refill_queue.pop()
|
||||
if refill_type == MM2EnergyLinkType.Life:
|
||||
exchange_rate = HP_EXCHANGE_RATE
|
||||
elif refill_type == MM2EnergyLinkType.OneUP:
|
||||
exchange_rate = ONEUP_EXCHANGE_RATE
|
||||
else:
|
||||
exchange_rate = WEAPON_EXCHANGE_RATE
|
||||
pool = ctx.stored_data.get(f"EnergyLink{ctx.team}", 0)
|
||||
request = exchange_rate * refill_amount
|
||||
if request > pool:
|
||||
logger.warning(
|
||||
f"Not enough energy to fulfill the request. Maximum request: {pool // exchange_rate}")
|
||||
else:
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "Set", "key": f"EnergyLink{ctx.team}", "slot": ctx.slot, "operations":
|
||||
[{"operation": "add", "value": -request},
|
||||
{"operation": "max", "value": 0}]}])
|
||||
if refill_type == MM2EnergyLinkType.Life:
|
||||
refill_ptr = MM2_HEALTH
|
||||
elif refill_type == MM2EnergyLinkType.OneUP:
|
||||
refill_ptr = MM2_LIVES
|
||||
else:
|
||||
refill_ptr = MM2_WEAPON_ENERGY - 1 + refill_type
|
||||
current_value = (await read(ctx.bizhawk_ctx, [(refill_ptr, 1, "RAM")]))[0][0]
|
||||
new_value = min(0x1C if refill_type != MM2EnergyLinkType.OneUP else 99, current_value + refill_amount)
|
||||
writes.append((refill_ptr, new_value.to_bytes(1, "little"), "RAM"))
|
||||
|
||||
if len(self.item_queue):
|
||||
item = self.item_queue.pop(0)
|
||||
idx = item.item & 0xF
|
||||
if idx == 0:
|
||||
# 1-Up
|
||||
current_lives = lives[0]
|
||||
if current_lives > 99:
|
||||
self.item_queue.append(item)
|
||||
else:
|
||||
current_lives += 1
|
||||
writes.append((MM2_LIVES, current_lives.to_bytes(1, 'little'), "RAM"))
|
||||
writes.extend(get_sfx_writes(0x42))
|
||||
elif idx == 1:
|
||||
self.weapon_energy += 0xE
|
||||
writes.extend(get_sfx_writes(0x28))
|
||||
elif idx == 2:
|
||||
self.health_energy += 0xE
|
||||
writes.extend(get_sfx_writes(0x28))
|
||||
elif idx == 3:
|
||||
# E-Tank
|
||||
# visuals only allow 4, but we're gonna go up to 9 anyway? May change
|
||||
current_tanks = e_tanks[0]
|
||||
if current_tanks < 9:
|
||||
current_tanks += 1
|
||||
writes.append((MM2_E_TANKS, current_tanks.to_bytes(1, 'little'), "RAM"))
|
||||
writes.extend(get_sfx_writes(0x42))
|
||||
else:
|
||||
self.item_queue.append(item)
|
||||
|
||||
await write(ctx.bizhawk_ctx, writes)
|
||||
|
||||
new_checks = []
|
||||
# check for locations
|
||||
for i in range(8):
|
||||
flag = 1 << i
|
||||
if robot_masters_defeated[0] & flag:
|
||||
wep_id = 0x880101 + i
|
||||
if wep_id not in ctx.checked_locations:
|
||||
new_checks.append(wep_id)
|
||||
|
||||
for i in range(3):
|
||||
flag = 1 << i
|
||||
if items_acquired[0] & flag:
|
||||
itm_id = 0x880111 + i
|
||||
if itm_id not in ctx.checked_locations:
|
||||
new_checks.append(itm_id)
|
||||
|
||||
for i in range(0xD):
|
||||
rbm_id = 0x880001 + i
|
||||
if completed_stages[i] != 0:
|
||||
if rbm_id not in ctx.checked_locations:
|
||||
new_checks.append(rbm_id)
|
||||
|
||||
for consumable in MM2_CONSUMABLE_TABLE:
|
||||
if consumable not in ctx.checked_locations:
|
||||
is_checked = consumable_checks[MM2_CONSUMABLE_TABLE[consumable][0]] \
|
||||
& MM2_CONSUMABLE_TABLE[consumable][1]
|
||||
if is_checked:
|
||||
new_checks.append(consumable)
|
||||
|
||||
for new_check_id in new_checks:
|
||||
ctx.locations_checked.add(new_check_id)
|
||||
location = ctx.location_names.lookup_in_game(new_check_id)
|
||||
nes_logger.info(
|
||||
f'New Check: {location} ({len(ctx.locations_checked)}/'
|
||||
f'{len(ctx.missing_locations) + len(ctx.checked_locations)})')
|
||||
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [new_check_id]}])
|
||||
276
worlds/mm2/color.py
Normal file
276
worlds/mm2/color.py
Normal file
@@ -0,0 +1,276 @@
|
||||
from typing import Dict, Tuple, List, TYPE_CHECKING, Union
|
||||
from . import names
|
||||
from zlib import crc32
|
||||
import struct
|
||||
import logging
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import MM2World
|
||||
from .rom import MM2ProcedurePatch
|
||||
|
||||
HTML_TO_NES: Dict[str, int] = {
|
||||
"SNOW": 0x20,
|
||||
"LINEN": 0x36,
|
||||
"SEASHELL": 0x36,
|
||||
"AZURE": 0x3C,
|
||||
"LAVENDER": 0x33,
|
||||
"WHITE": 0x30,
|
||||
"BLACK": 0x0F,
|
||||
"GREY": 0x00,
|
||||
"GRAY": 0x00,
|
||||
"ROYALBLUE": 0x12,
|
||||
"BLUE": 0x11,
|
||||
"SKYBLUE": 0x21,
|
||||
"LIGHTBLUE": 0x31,
|
||||
"TURQUOISE": 0x2B,
|
||||
"CYAN": 0x2C,
|
||||
"AQUAMARINE": 0x3B,
|
||||
"DARKGREEN": 0x0A,
|
||||
"GREEN": 0x1A,
|
||||
"YELLOW": 0x28,
|
||||
"GOLD": 0x28,
|
||||
"WHEAT": 0x37,
|
||||
"TAN": 0x37,
|
||||
"CHOCOLATE": 0x07,
|
||||
"BROWN": 0x07,
|
||||
"SALMON": 0x26,
|
||||
"ORANGE": 0x27,
|
||||
"CORAL": 0x36,
|
||||
"TOMATO": 0x16,
|
||||
"RED": 0x16,
|
||||
"PINK": 0x25,
|
||||
"MAROON": 0x06,
|
||||
"MAGENTA": 0x24,
|
||||
"FUSCHIA": 0x24,
|
||||
"VIOLET": 0x24,
|
||||
"PLUM": 0x33,
|
||||
"PURPLE": 0x14,
|
||||
"THISTLE": 0x34,
|
||||
"DARKBLUE": 0x01,
|
||||
"SILVER": 0x10,
|
||||
"NAVY": 0x02,
|
||||
"TEAL": 0x1C,
|
||||
"OLIVE": 0x18,
|
||||
"LIME": 0x2A,
|
||||
"AQUA": 0x2C,
|
||||
# can add more as needed
|
||||
}
|
||||
|
||||
MM2_COLORS: Dict[str, Tuple[int, int]] = {
|
||||
names.atomic_fire: (0x28, 0x15),
|
||||
names.air_shooter: (0x20, 0x11),
|
||||
names.leaf_shield: (0x20, 0x19),
|
||||
names.bubble_lead: (0x20, 0x00),
|
||||
names.time_stopper: (0x34, 0x25),
|
||||
names.quick_boomerang: (0x34, 0x14),
|
||||
names.metal_blade: (0x37, 0x18),
|
||||
names.crash_bomber: (0x20, 0x26),
|
||||
names.item_1: (0x20, 0x16),
|
||||
names.item_2: (0x20, 0x16),
|
||||
names.item_3: (0x20, 0x16),
|
||||
names.heat_man_stage: (0x28, 0x15),
|
||||
names.air_man_stage: (0x28, 0x11),
|
||||
names.wood_man_stage: (0x36, 0x17),
|
||||
names.bubble_man_stage: (0x30, 0x19),
|
||||
names.quick_man_stage: (0x28, 0x15),
|
||||
names.flash_man_stage: (0x30, 0x12),
|
||||
names.metal_man_stage: (0x28, 0x15),
|
||||
names.crash_man_stage: (0x30, 0x16)
|
||||
}
|
||||
|
||||
MM2_KNOWN_COLORS: Dict[str, Tuple[int, int]] = {
|
||||
**MM2_COLORS,
|
||||
# Street Fighter, technically
|
||||
"Hadouken": (0x3C, 0x11),
|
||||
"Shoryuken": (0x38, 0x16),
|
||||
# X Series
|
||||
"Z-Saber": (0x20, 0x16),
|
||||
# X1
|
||||
"Homing Torpedo": (0x3D, 0x37),
|
||||
"Chameleon Sting": (0x3B, 0x1A),
|
||||
"Rolling Shield": (0x3A, 0x25),
|
||||
"Fire Wave": (0x37, 0x26),
|
||||
"Storm Tornado": (0x34, 0x14),
|
||||
"Electric Spark": (0x3D, 0x28),
|
||||
"Boomerang Cutter": (0x3B, 0x2D),
|
||||
"Shotgun Ice": (0x28, 0x2C),
|
||||
# X2
|
||||
"Crystal Hunter": (0x33, 0x21),
|
||||
"Bubble Splash": (0x35, 0x28),
|
||||
"Spin Wheel": (0x34, 0x1B),
|
||||
"Silk Shot": (0x3B, 0x27),
|
||||
"Sonic Slicer": (0x27, 0x01),
|
||||
"Strike Chain": (0x30, 0x23),
|
||||
"Magnet Mine": (0x28, 0x2D),
|
||||
"Speed Burner": (0x31, 0x16),
|
||||
# X3
|
||||
"Acid Burst": (0x28, 0x2A),
|
||||
"Tornado Fang": (0x28, 0x2C),
|
||||
"Triad Thunder": (0x2B, 0x23),
|
||||
"Spinning Blade": (0x20, 0x16),
|
||||
"Ray Splasher": (0x28, 0x17),
|
||||
"Gravity Well": (0x38, 0x14),
|
||||
"Parasitic Bomb": (0x31, 0x28),
|
||||
"Frost Shield": (0x23, 0x2C),
|
||||
}
|
||||
|
||||
palette_pointers: Dict[str, List[int]] = {
|
||||
"Mega Buster": [0x3D314],
|
||||
"Atomic Fire": [0x3D318],
|
||||
"Air Shooter": [0x3D31C],
|
||||
"Leaf Shield": [0x3D320],
|
||||
"Bubble Lead": [0x3D324],
|
||||
"Quick Boomerang": [0x3D328],
|
||||
"Time Stopper": [0x3D32C],
|
||||
"Metal Blade": [0x3D330],
|
||||
"Crash Bomber": [0x3D334],
|
||||
"Item 1": [0x3D338],
|
||||
"Item 2": [0x3D33C],
|
||||
"Item 3": [0x3D340],
|
||||
"Heat Man": [0x34B6, 0x344F7],
|
||||
"Air Man": [0x74B6, 0x344FF],
|
||||
"Wood Man": [0xB4EC, 0x34507],
|
||||
"Bubble Man": [0xF4B6, 0x3450F],
|
||||
"Quick Man": [0x134C8, 0x34517],
|
||||
"Flash Man": [0x174B6, 0x3451F],
|
||||
"Metal Man": [0x1B4A4, 0x34527],
|
||||
"Crash Man": [0x1F4EC, 0x3452F],
|
||||
}
|
||||
|
||||
|
||||
def add_color_to_mm2(name: str, color: Tuple[int, int]) -> None:
|
||||
"""
|
||||
Add a color combo for Mega Man 2 to recognize as the color to display for a given item.
|
||||
For information on available colors: https://www.nesdev.org/wiki/PPU_palettes#2C02
|
||||
"""
|
||||
MM2_KNOWN_COLORS[name] = validate_colors(*color)
|
||||
|
||||
|
||||
def extrapolate_color(color: int) -> Tuple[int, int]:
|
||||
if color > 0x1F:
|
||||
color_1 = color
|
||||
color_2 = color_1 - 0x10
|
||||
else:
|
||||
color_2 = color
|
||||
color_1 = color_2 + 0x10
|
||||
return color_1, color_2
|
||||
|
||||
|
||||
def validate_colors(color_1: int, color_2: int, allow_match: bool = False) -> Tuple[int, int]:
|
||||
# Black should be reserved for outlines, a gray should suffice
|
||||
if color_1 in [0x0D, 0x0E, 0x0F, 0x1E, 0x2E, 0x3E, 0x1F, 0x2F, 0x3F]:
|
||||
color_1 = 0x10
|
||||
if color_2 in [0x0D, 0x0E, 0x0F, 0x1E, 0x2E, 0x3E, 0x1F, 0x2F, 0x3F]:
|
||||
color_2 = 0x10
|
||||
|
||||
# one final check, make sure we don't have two matching
|
||||
if not allow_match and color_1 == color_2:
|
||||
color_1 = 0x30 # color 1 to white works with about any paired color
|
||||
|
||||
return color_1, color_2
|
||||
|
||||
|
||||
def get_colors_for_item(name: str) -> Tuple[int, int]:
|
||||
if name in MM2_KNOWN_COLORS:
|
||||
return MM2_KNOWN_COLORS[name]
|
||||
|
||||
check_colors = {color: color in name.upper().replace(" ", "") for color in HTML_TO_NES}
|
||||
colors = [color for color in check_colors if check_colors[color]]
|
||||
if colors:
|
||||
# we have at least one color pattern matched
|
||||
if len(colors) > 1:
|
||||
# we have at least 2
|
||||
color_1 = HTML_TO_NES[colors[0]]
|
||||
color_2 = HTML_TO_NES[colors[1]]
|
||||
else:
|
||||
color_1, color_2 = extrapolate_color(HTML_TO_NES[colors[0]])
|
||||
else:
|
||||
# generate hash
|
||||
crc_hash = crc32(name.encode("utf-8"))
|
||||
hash_color = struct.pack("I", crc_hash)
|
||||
color_1 = hash_color[0] % 0x3F
|
||||
color_2 = hash_color[1] % 0x3F
|
||||
|
||||
if color_1 < color_2:
|
||||
temp = color_1
|
||||
color_1 = color_2
|
||||
color_2 = temp
|
||||
|
||||
color_1, color_2 = validate_colors(color_1, color_2)
|
||||
|
||||
return color_1, color_2
|
||||
|
||||
|
||||
def parse_color(colors: List[str]) -> Tuple[int, int]:
|
||||
color_a = colors[0]
|
||||
if color_a.startswith("$"):
|
||||
color_1 = int(color_a[1:], 16)
|
||||
else:
|
||||
# assume it's in our list of colors
|
||||
color_1 = HTML_TO_NES[color_a.upper()]
|
||||
|
||||
if len(colors) == 1:
|
||||
color_1, color_2 = extrapolate_color(color_1)
|
||||
else:
|
||||
color_b = colors[1]
|
||||
if color_b.startswith("$"):
|
||||
color_2 = int(color_b[1:], 16)
|
||||
else:
|
||||
color_2 = HTML_TO_NES[color_b.upper()]
|
||||
return color_1, color_2
|
||||
|
||||
|
||||
def write_palette_shuffle(world: "MM2World", rom: "MM2ProcedurePatch") -> None:
|
||||
palette_shuffle: Union[int, str] = world.options.palette_shuffle.value
|
||||
palettes_to_write: Dict[str, Tuple[int, int]] = {}
|
||||
if isinstance(palette_shuffle, str):
|
||||
color_sets = palette_shuffle.split(";")
|
||||
if len(color_sets) == 1:
|
||||
palette_shuffle = world.options.palette_shuffle.option_none
|
||||
# singularity is more correct, but this is faster
|
||||
else:
|
||||
palette_shuffle = world.options.palette_shuffle.options[color_sets.pop()]
|
||||
for color_set in color_sets:
|
||||
if "-" in color_set:
|
||||
character, color = color_set.split("-")
|
||||
if character.title() not in palette_pointers:
|
||||
logging.warning(f"Player {world.multiworld.get_player_name(world.player)} "
|
||||
f"attempted to set color for unrecognized option {character}")
|
||||
colors = color.split("|")
|
||||
real_colors = validate_colors(*parse_color(colors), allow_match=True)
|
||||
palettes_to_write[character.title()] = real_colors
|
||||
else:
|
||||
# If color is provided with no character, assume singularity
|
||||
colors = color_set.split("|")
|
||||
real_colors = validate_colors(*parse_color(colors), allow_match=True)
|
||||
for character in palette_pointers:
|
||||
palettes_to_write[character] = real_colors
|
||||
# Now we handle the real values
|
||||
if palette_shuffle == 1:
|
||||
shuffled_colors = list(MM2_COLORS.values())
|
||||
shuffled_colors.append((0x2C, 0x11)) # Mega Buster
|
||||
world.random.shuffle(shuffled_colors)
|
||||
for character in palette_pointers:
|
||||
if character not in palettes_to_write:
|
||||
palettes_to_write[character] = shuffled_colors.pop()
|
||||
elif palette_shuffle > 1:
|
||||
if palette_shuffle == 2:
|
||||
for character in palette_pointers:
|
||||
if character not in palettes_to_write:
|
||||
real_colors = validate_colors(world.random.randint(0, 0x3F), world.random.randint(0, 0x3F))
|
||||
palettes_to_write[character] = real_colors
|
||||
else:
|
||||
# singularity
|
||||
real_colors = validate_colors(world.random.randint(0, 0x3F), world.random.randint(0, 0x3F))
|
||||
for character in palette_pointers:
|
||||
if character not in palettes_to_write:
|
||||
palettes_to_write[character] = real_colors
|
||||
|
||||
for character in palettes_to_write:
|
||||
for pointer in palette_pointers[character]:
|
||||
rom.write_bytes(pointer, bytes(palettes_to_write[character]))
|
||||
|
||||
if character == "Atomic Fire":
|
||||
# special case, we need to update Atomic Fire's flashing routine
|
||||
rom.write_byte(0x3DE4A, palettes_to_write[character][1])
|
||||
rom.write_byte(0x3DE4C, palettes_to_write[character][1])
|
||||
BIN
worlds/mm2/data/mm2_basepatch.bsdiff4
Normal file
BIN
worlds/mm2/data/mm2_basepatch.bsdiff4
Normal file
Binary file not shown.
114
worlds/mm2/docs/en_Mega Man 2.md
Normal file
114
worlds/mm2/docs/en_Mega Man 2.md
Normal file
@@ -0,0 +1,114 @@
|
||||
# Mega Man 2
|
||||
|
||||
## Where is the options page?
|
||||
|
||||
The [player options page for this game](../player-options) contains all the options you need to configure and export a
|
||||
config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
|
||||
Weapons received from Robot Masters, access to each individual stage, and Items from Dr. Light are randomized
|
||||
into the multiworld. Access to the Wily Stages is locked behind receiving Item 1, 2, and 3. The game is completed when
|
||||
viewing the ending sequence after defeating the Alien.
|
||||
|
||||
## What Mega Man 2 items can appear in other players' worlds?
|
||||
- Robot Master weapons
|
||||
- Robot Master Access Codes (stage access)
|
||||
- Items 1/2/3
|
||||
- 1-Ups
|
||||
- E-Tanks
|
||||
- Health Energy (L)
|
||||
- Weapon Energy (L)
|
||||
|
||||
## What is considered a location check in Mega Man 2?
|
||||
- The defeat of a Robot Master or Wily Boss
|
||||
- Receiving a weapon or item from Dr. Light
|
||||
- Optionally, 1-Ups and E-Tanks present within stages
|
||||
- Optionally, Weapon and Health Energy pickups present within stages
|
||||
|
||||
## When the player receives an item, what happens?
|
||||
A sound effect will play based on the type of item received, and the effects of the item will be immediately applied,
|
||||
such as unlocking the use of a weapon mid-stage. If the effects of the item cannot be fully applied (such as receiving
|
||||
Health Energy while at full health), the leftover amount is withheld until it can be applied.
|
||||
|
||||
## What is EnergyLink?
|
||||
EnergyLink is an energy storage supported by certain games that is shared across all worlds in a multiworld. In Mega Man
|
||||
2, when enabled, drops from enemies are not applied directly to Mega Man and are instead deposited into the EnergyLink.
|
||||
Half of the energy that would be gained is lost upon transfer to the EnergyLink.
|
||||
|
||||
Energy from the EnergyLink storage can be converted into health, weapon energy, and lives at different conversion rates.
|
||||
You can find out how much of each type you can pull using the `/pool` command in the client. Additionally, you can have it
|
||||
automatically pull from the EnergyLink storage to keep Mega Man healed using the `/autoheal` command in the client.
|
||||
Finally, you can use the `/request` command to request a certain type of energy from the storage.
|
||||
|
||||
## Plando Palettes
|
||||
The palette shuffle option supports specifying a specific palette for a given weapon/Robot Master. The format for doing
|
||||
so is `Character-Color1|Color2;Option`. Character is the individual that this should apply to, and can only be one of
|
||||
the following:
|
||||
- Mega Buster
|
||||
- Atomic Fire
|
||||
- Air Shooter
|
||||
- Leaf Shield
|
||||
- Bubble Lead
|
||||
- Quick Boomerang
|
||||
- Time Stopper
|
||||
- Metal Blade
|
||||
- Crash Bomber
|
||||
- Item 1
|
||||
- Item 2
|
||||
- Item 3
|
||||
- Heat Man
|
||||
- Air Man
|
||||
- Wood Man
|
||||
- Bubble Man
|
||||
- Quick Man
|
||||
- Flash Man
|
||||
- Metal Man
|
||||
- Crash Man
|
||||
|
||||
Colors attempt to map a list of HTML-defined colors to what the NES can render. A full list of applicable colors can be
|
||||
found [here](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/mm2/color.py#L11). Alternatively, colors can
|
||||
be supplied directly using `$xx` format. A full list of NES colors can be found [here](https://www.nesdev.org/wiki/PPU_palettes#2C02).
|
||||
|
||||
You can also pass only one color (such as `Mega Buster-Red`) and it will interpret a second color based off of the color
|
||||
given. Additionally, passing only colors (such as `Red|Blue`) and not any specific boss/weapon will apply that color to
|
||||
all weapons/bosses that did not have a prior color specified.
|
||||
|
||||
The option is the method to be used to set the palettes of the remaining bosses/weapons, and will not overwrite any
|
||||
plando placements.
|
||||
|
||||
## Plando Weaknesses
|
||||
Plando Weaknesses allows you to override the amount of damage a boss should take from a given weapon, ignoring prior
|
||||
weaknesses generated by strict/random weakness options. Formatting for this is as follows:
|
||||
```yaml
|
||||
plando_weakness:
|
||||
Air Man:
|
||||
Atomic Fire: 0
|
||||
Bubble Lead: 4
|
||||
```
|
||||
This would cause Air Man to take 4 damage from Bubble Lead, and 0 from Atomic Fire.
|
||||
|
||||
Note: it is possible that plando weakness is not be respected should the plando create a situation in which the game
|
||||
becomes impossible to complete. In this situation, the damage would be boosted to the minimum required to defeat the
|
||||
Robot Master.
|
||||
|
||||
|
||||
## Unique Local Commands
|
||||
- `/pool` Only present with EnergyLink, prints the max amount of each type of request that could be fulfilled.
|
||||
- `/autoheal` Only present with EnergyLink, will automatically drain energy from the EnergyLink in order to
|
||||
restore Mega Man's health.
|
||||
- `/request <amount> <type>` Only present with EnergyLink, sends a request of a certain type of energy to be pulled from
|
||||
the EnergyLink. Types are as follows:
|
||||
- `HP` Health
|
||||
- `AF` Atomic Fire
|
||||
- `AS` Air Shooter
|
||||
- `LS` Leaf Shield
|
||||
- `BL` Bubble Lead
|
||||
- `QB` Quick Boomerang
|
||||
- `TS` Time Stopper
|
||||
- `MB` Metal Blade
|
||||
- `CB` Crash Bomber
|
||||
- `I1` Item 1
|
||||
- `I2` Item 2
|
||||
- `I3` Item 3
|
||||
- `1U` Lives
|
||||
53
worlds/mm2/docs/setup_en.md
Normal file
53
worlds/mm2/docs/setup_en.md
Normal file
@@ -0,0 +1,53 @@
|
||||
# Mega Man 2 Setup Guide
|
||||
|
||||
## Required Software
|
||||
|
||||
- [Archipelago](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
- An English Mega Man 2 ROM. Alternatively, the [Mega Man Legacy Collection](https://store.steampowered.com/app/363440/Mega_Man_Legacy_Collection/) on Steam.
|
||||
- [BizHawk](https://tasvideos.org/BizHawk/ReleaseHistory) 2.7 or later
|
||||
|
||||
### Configuring Bizhawk
|
||||
|
||||
Once you have installed BizHawk, open `EmuHawk.exe` and change the following settings:
|
||||
|
||||
- If you're using BizHawk 2.7 or 2.8, go to `Config > Customize`. On the Advanced tab, switch the Lua Core from
|
||||
`NLua+KopiLua` to `Lua+LuaInterface`, then restart EmuHawk. (If you're using BizHawk 2.9, you can skip this step.)
|
||||
- Under `Config > Customize`, check the "Run in background" option to prevent disconnecting from the client while you're
|
||||
tabbed out of EmuHawk.
|
||||
- Open a `.nes` file in EmuHawk and go to `Config > Controllers…` to configure your inputs. If you can't click
|
||||
`Controllers…`, load any `.nes` ROM first.
|
||||
- Consider clearing keybinds in `Config > Hotkeys…` if you don't intend to use them. Select the keybind and press Esc to
|
||||
clear it.
|
||||
|
||||
## Generating and Patching a Game
|
||||
|
||||
1. Create your options file (YAML). You can make one on the
|
||||
[Mega Man 2 options page](../../../games/Mega%20Man%202/player-options).
|
||||
2. Follow the general Archipelago instructions for [generating a game](../../Archipelago/setup/en#generating-a-game).
|
||||
This will generate an output file for you. Your patch file will have the `.apmm2` file extension.
|
||||
3. Open `ArchipelagoLauncher.exe`
|
||||
4. Select "Open Patch" on the left side and select your patch file.
|
||||
5. If this is your first time patching, you will be prompted to locate your vanilla ROM. If you are using the Legacy
|
||||
Collection, provide `Proteus.exe` in place of your rom.
|
||||
6. A patched `.nes` file will be created in the same place as the patch file.
|
||||
7. On your first time opening a patch with BizHawk Client, you will also be asked to locate `EmuHawk.exe` in your
|
||||
BizHawk install.
|
||||
|
||||
## Connecting to a Server
|
||||
|
||||
By default, opening a patch file will do steps 1-5 below for you automatically. Even so, keep them in your memory just
|
||||
in case you have to close and reopen a window mid-game for some reason.
|
||||
|
||||
1. Mega Man 2 uses Archipelago's BizHawk Client. If the client isn't still open from when you patched your game,
|
||||
you can re-open it from the launcher.
|
||||
2. Ensure EmuHawk is running the patched ROM.
|
||||
3. In EmuHawk, go to `Tools > Lua Console`. This window must stay open while playing.
|
||||
4. In the Lua Console window, go to `Script > Open Script…`.
|
||||
5. Navigate to your Archipelago install folder and open `data/lua/connector_bizhawk_generic.lua`.
|
||||
6. The emulator and client will eventually connect to each other. The BizHawk Client window should indicate that it
|
||||
connected and recognized Mega Man 2.
|
||||
7. To connect the client to the server, enter your room's address and port (e.g. `archipelago.gg:38281`) into the
|
||||
top text field of the client and click Connect.
|
||||
|
||||
You should now be able to receive and send items. You'll need to do these steps every time you want to reconnect. It is
|
||||
perfectly safe to make progress offline; everything will re-sync when you reconnect.
|
||||
72
worlds/mm2/items.py
Normal file
72
worlds/mm2/items.py
Normal file
@@ -0,0 +1,72 @@
|
||||
from BaseClasses import Item
|
||||
from typing import NamedTuple, Dict
|
||||
from . import names
|
||||
|
||||
|
||||
class ItemData(NamedTuple):
|
||||
code: int
|
||||
progression: bool
|
||||
useful: bool = False # primarily use this for incredibly useful items of their class, like Metal Blade
|
||||
skip_balancing: bool = False
|
||||
|
||||
|
||||
class MM2Item(Item):
|
||||
game = "Mega Man 2"
|
||||
|
||||
|
||||
robot_master_weapon_table = {
|
||||
names.atomic_fire: ItemData(0x880001, True),
|
||||
names.air_shooter: ItemData(0x880002, True),
|
||||
names.leaf_shield: ItemData(0x880003, True),
|
||||
names.bubble_lead: ItemData(0x880004, True),
|
||||
names.quick_boomerang: ItemData(0x880005, True),
|
||||
names.time_stopper: ItemData(0x880006, True, True),
|
||||
names.metal_blade: ItemData(0x880007, True, True),
|
||||
names.crash_bomber: ItemData(0x880008, True),
|
||||
}
|
||||
|
||||
stage_access_table = {
|
||||
names.heat_man_stage: ItemData(0x880101, True),
|
||||
names.air_man_stage: ItemData(0x880102, True),
|
||||
names.wood_man_stage: ItemData(0x880103, True),
|
||||
names.bubble_man_stage: ItemData(0x880104, True),
|
||||
names.quick_man_stage: ItemData(0x880105, True),
|
||||
names.flash_man_stage: ItemData(0x880106, True),
|
||||
names.metal_man_stage: ItemData(0x880107, True),
|
||||
names.crash_man_stage: ItemData(0x880108, True),
|
||||
}
|
||||
|
||||
item_item_table = {
|
||||
names.item_1: ItemData(0x880011, True, True, True),
|
||||
names.item_2: ItemData(0x880012, True, True, True),
|
||||
names.item_3: ItemData(0x880013, True, True, True)
|
||||
}
|
||||
|
||||
filler_item_table = {
|
||||
names.one_up: ItemData(0x880020, False),
|
||||
names.weapon_energy: ItemData(0x880021, False),
|
||||
names.health_energy: ItemData(0x880022, False),
|
||||
names.e_tank: ItemData(0x880023, False, True),
|
||||
}
|
||||
|
||||
filler_item_weights = {
|
||||
names.one_up: 1,
|
||||
names.weapon_energy: 4,
|
||||
names.health_energy: 1,
|
||||
names.e_tank: 2,
|
||||
}
|
||||
|
||||
item_table = {
|
||||
**robot_master_weapon_table,
|
||||
**stage_access_table,
|
||||
**item_item_table,
|
||||
**filler_item_table,
|
||||
}
|
||||
|
||||
item_names = {
|
||||
"Weapons": {name for name in robot_master_weapon_table.keys()},
|
||||
"Stages": {name for name in stage_access_table.keys()},
|
||||
"Items": {name for name in item_item_table.keys()}
|
||||
}
|
||||
|
||||
lookup_item_to_id: Dict[str, int] = {item_name: data.code for item_name, data in item_table.items()}
|
||||
239
worlds/mm2/locations.py
Normal file
239
worlds/mm2/locations.py
Normal file
@@ -0,0 +1,239 @@
|
||||
from BaseClasses import Location, Region
|
||||
from typing import Dict, Tuple, Optional
|
||||
from . import names
|
||||
|
||||
|
||||
class MM2Location(Location):
|
||||
game = "Mega Man 2"
|
||||
|
||||
|
||||
class MM2Region(Region):
|
||||
game = "Mega Man 2"
|
||||
|
||||
|
||||
heat_man_locations: Dict[str, Optional[int]] = {
|
||||
names.heat_man: 0x880001,
|
||||
names.atomic_fire_get: 0x880101,
|
||||
names.item_1_get: 0x880111,
|
||||
}
|
||||
|
||||
air_man_locations: Dict[str, Optional[int]] = {
|
||||
names.air_man: 0x880002,
|
||||
names.air_shooter_get: 0x880102,
|
||||
names.item_2_get: 0x880112
|
||||
}
|
||||
|
||||
wood_man_locations: Dict[str, Optional[int]] = {
|
||||
names.wood_man: 0x880003,
|
||||
names.leaf_shield_get: 0x880103
|
||||
}
|
||||
|
||||
bubble_man_locations: Dict[str, Optional[int]] = {
|
||||
names.bubble_man: 0x880004,
|
||||
names.bubble_lead_get: 0x880104
|
||||
}
|
||||
|
||||
quick_man_locations: Dict[str, Optional[int]] = {
|
||||
names.quick_man: 0x880005,
|
||||
names.quick_boomerang_get: 0x880105,
|
||||
}
|
||||
|
||||
flash_man_locations: Dict[str, Optional[int]] = {
|
||||
names.flash_man: 0x880006,
|
||||
names.time_stopper_get: 0x880106,
|
||||
names.item_3_get: 0x880113,
|
||||
}
|
||||
|
||||
metal_man_locations: Dict[str, Optional[int]] = {
|
||||
names.metal_man: 0x880007,
|
||||
names.metal_blade_get: 0x880107
|
||||
}
|
||||
|
||||
crash_man_locations: Dict[str, Optional[int]] = {
|
||||
names.crash_man: 0x880008,
|
||||
names.crash_bomber_get: 0x880108
|
||||
}
|
||||
|
||||
wily_1_locations: Dict[str, Optional[int]] = {
|
||||
names.wily_1: 0x880009,
|
||||
names.wily_stage_1: None
|
||||
}
|
||||
|
||||
wily_2_locations: Dict[str, Optional[int]] = {
|
||||
names.wily_2: 0x88000A,
|
||||
names.wily_stage_2: None
|
||||
}
|
||||
|
||||
wily_3_locations: Dict[str, Optional[int]] = {
|
||||
names.wily_3: 0x88000B,
|
||||
names.wily_stage_3: None
|
||||
}
|
||||
|
||||
wily_4_locations: Dict[str, Optional[int]] = {
|
||||
names.wily_4: 0x88000C,
|
||||
names.wily_stage_4: None
|
||||
}
|
||||
|
||||
wily_5_locations: Dict[str, Optional[int]] = {
|
||||
names.wily_5: 0x88000D,
|
||||
names.wily_stage_5: None
|
||||
}
|
||||
|
||||
wily_6_locations: Dict[str, Optional[int]] = {
|
||||
names.dr_wily: None
|
||||
}
|
||||
|
||||
etank_1ups: Dict[str, Dict[str, Optional[int]]] = {
|
||||
"Heat Man Stage": {
|
||||
names.heat_man_c1: 0x880201,
|
||||
},
|
||||
"Quick Man Stage": {
|
||||
names.quick_man_c1: 0x880202,
|
||||
names.quick_man_c2: 0x880203,
|
||||
names.quick_man_c3: 0x880204,
|
||||
names.quick_man_c7: 0x880208,
|
||||
},
|
||||
"Flash Man Stage": {
|
||||
names.flash_man_c2: 0x88020B,
|
||||
names.flash_man_c6: 0x88020F,
|
||||
},
|
||||
"Metal Man Stage": {
|
||||
names.metal_man_c1: 0x880210,
|
||||
names.metal_man_c2: 0x880211,
|
||||
names.metal_man_c3: 0x880212,
|
||||
},
|
||||
"Crash Man Stage": {
|
||||
names.crash_man_c2: 0x880214,
|
||||
names.crash_man_c3: 0x880215,
|
||||
},
|
||||
"Wily Stage 1": {
|
||||
names.wily_1_c1: 0x880216,
|
||||
},
|
||||
"Wily Stage 2": {
|
||||
names.wily_2_c3: 0x88021A,
|
||||
names.wily_2_c4: 0x88021B,
|
||||
names.wily_2_c5: 0x88021C,
|
||||
names.wily_2_c6: 0x88021D,
|
||||
},
|
||||
"Wily Stage 3": {
|
||||
names.wily_3_c2: 0x880220,
|
||||
},
|
||||
"Wily Stage 4": {
|
||||
names.wily_4_c3: 0x880225,
|
||||
names.wily_4_c4: 0x880226,
|
||||
}
|
||||
}
|
||||
|
||||
energy_pickups: Dict[str, Dict[str, Optional[int]]] = {
|
||||
"Quick Man Stage": {
|
||||
names.quick_man_c4: 0x880205,
|
||||
names.quick_man_c5: 0x880206,
|
||||
names.quick_man_c6: 0x880207,
|
||||
names.quick_man_c8: 0x880209,
|
||||
},
|
||||
"Flash Man Stage": {
|
||||
names.flash_man_c1: 0x88020A,
|
||||
names.flash_man_c3: 0x88020C,
|
||||
names.flash_man_c4: 0x88020D,
|
||||
names.flash_man_c5: 0x88020E,
|
||||
},
|
||||
"Crash Man Stage": {
|
||||
names.crash_man_c1: 0x880213,
|
||||
},
|
||||
"Wily Stage 1": {
|
||||
names.wily_1_c2: 0x880217,
|
||||
},
|
||||
"Wily Stage 2": {
|
||||
names.wily_2_c1: 0x880218,
|
||||
names.wily_2_c2: 0x880219,
|
||||
names.wily_2_c7: 0x88021E,
|
||||
names.wily_2_c8: 0x880227,
|
||||
names.wily_2_c9: 0x880228,
|
||||
names.wily_2_c10: 0x880229,
|
||||
names.wily_2_c11: 0x88022A,
|
||||
names.wily_2_c12: 0x88022B,
|
||||
names.wily_2_c13: 0x88022C,
|
||||
names.wily_2_c14: 0x88022D,
|
||||
names.wily_2_c15: 0x88022E,
|
||||
names.wily_2_c16: 0x88022F,
|
||||
},
|
||||
"Wily Stage 3": {
|
||||
names.wily_3_c1: 0x88021F,
|
||||
names.wily_3_c3: 0x880221,
|
||||
names.wily_3_c4: 0x880222,
|
||||
},
|
||||
"Wily Stage 4": {
|
||||
names.wily_4_c1: 0x880223,
|
||||
names.wily_4_c2: 0x880224,
|
||||
}
|
||||
}
|
||||
|
||||
mm2_regions: Dict[str, Tuple[Tuple[str, ...], Dict[str, Optional[int]], Optional[str]]] = {
|
||||
"Heat Man Stage": ((names.heat_man_stage,), heat_man_locations, None),
|
||||
"Air Man Stage": ((names.air_man_stage,), air_man_locations, None),
|
||||
"Wood Man Stage": ((names.wood_man_stage,), wood_man_locations, None),
|
||||
"Bubble Man Stage": ((names.bubble_man_stage,), bubble_man_locations, None),
|
||||
"Quick Man Stage": ((names.quick_man_stage,), quick_man_locations, None),
|
||||
"Flash Man Stage": ((names.flash_man_stage,), flash_man_locations, None),
|
||||
"Metal Man Stage": ((names.metal_man_stage,), metal_man_locations, None),
|
||||
"Crash Man Stage": ((names.crash_man_stage,), crash_man_locations, None),
|
||||
"Wily Stage 1": ((names.item_1, names.item_2, names.item_3), wily_1_locations, None),
|
||||
"Wily Stage 2": ((names.wily_stage_1,), wily_2_locations, "Wily Stage 1"),
|
||||
"Wily Stage 3": ((names.wily_stage_2,), wily_3_locations, "Wily Stage 2"),
|
||||
"Wily Stage 4": ((names.wily_stage_3,), wily_4_locations, "Wily Stage 3"),
|
||||
"Wily Stage 5": ((names.wily_stage_4,), wily_5_locations, "Wily Stage 4"),
|
||||
"Wily Stage 6": ((names.wily_stage_5,), wily_6_locations, "Wily Stage 5")
|
||||
}
|
||||
|
||||
location_table: Dict[str, Optional[int]] = {
|
||||
**heat_man_locations,
|
||||
**air_man_locations,
|
||||
**wood_man_locations,
|
||||
**bubble_man_locations,
|
||||
**quick_man_locations,
|
||||
**flash_man_locations,
|
||||
**metal_man_locations,
|
||||
**crash_man_locations,
|
||||
**wily_1_locations,
|
||||
**wily_2_locations,
|
||||
**wily_3_locations,
|
||||
**wily_4_locations,
|
||||
**wily_5_locations,
|
||||
}
|
||||
|
||||
for table in etank_1ups:
|
||||
location_table.update(etank_1ups[table])
|
||||
|
||||
for table in energy_pickups:
|
||||
location_table.update(energy_pickups[table])
|
||||
|
||||
location_groups = {
|
||||
"Get Equipped": {
|
||||
names.atomic_fire_get,
|
||||
names.air_shooter_get,
|
||||
names.leaf_shield_get,
|
||||
names.bubble_lead_get,
|
||||
names.quick_boomerang_get,
|
||||
names.time_stopper_get,
|
||||
names.metal_blade_get,
|
||||
names.crash_bomber_get,
|
||||
names.item_1_get,
|
||||
names.item_2_get,
|
||||
names.item_3_get
|
||||
},
|
||||
"Heat Man Stage": {*heat_man_locations.keys(), *etank_1ups["Heat Man Stage"].keys()},
|
||||
"Air Man Stage": {*air_man_locations.keys()},
|
||||
"Wood Man Stage": {*wood_man_locations.keys()},
|
||||
"Bubble Man Stage": {*bubble_man_locations.keys()},
|
||||
"Quick Man Stage": {*quick_man_locations.keys(), *etank_1ups["Quick Man Stage"].keys(),
|
||||
*energy_pickups["Quick Man Stage"].keys()},
|
||||
"Flash Man Stage": {*flash_man_locations.keys(), *etank_1ups["Flash Man Stage"].keys(),
|
||||
*energy_pickups["Flash Man Stage"].keys()},
|
||||
"Metal Man Stage": {*metal_man_locations.keys(), *etank_1ups["Metal Man Stage"].keys()},
|
||||
"Crash Man Stage": {*crash_man_locations.keys(), *etank_1ups["Crash Man Stage"].keys(),
|
||||
*energy_pickups["Crash Man Stage"].keys()},
|
||||
"Wily 2 Weapon Energy": {names.wily_2_c8, names.wily_2_c9, names.wily_2_c10, names.wily_2_c11, names.wily_2_c12,
|
||||
names.wily_2_c13, names.wily_2_c14, names.wily_2_c15, names.wily_2_c16}
|
||||
}
|
||||
|
||||
lookup_location_to_id: Dict[str, int] = {location: idx for location, idx in location_table.items() if idx is not None}
|
||||
114
worlds/mm2/names.py
Normal file
114
worlds/mm2/names.py
Normal file
@@ -0,0 +1,114 @@
|
||||
# Robot Master Weapons
|
||||
crash_bomber = "Crash Bomber"
|
||||
metal_blade = "Metal Blade"
|
||||
quick_boomerang = "Quick Boomerang"
|
||||
bubble_lead = "Bubble Lead"
|
||||
atomic_fire = "Atomic Fire"
|
||||
leaf_shield = "Leaf Shield"
|
||||
time_stopper = "Time Stopper"
|
||||
air_shooter = "Air Shooter"
|
||||
|
||||
# Stage Entry
|
||||
crash_man_stage = "Crash Man Access Codes"
|
||||
metal_man_stage = "Metal Man Access Codes"
|
||||
quick_man_stage = "Quick Man Access Codes"
|
||||
bubble_man_stage = "Bubble Man Access Codes"
|
||||
heat_man_stage = "Heat Man Access Codes"
|
||||
wood_man_stage = "Wood Man Access Codes"
|
||||
flash_man_stage = "Flash Man Access Codes"
|
||||
air_man_stage = "Air Man Access Codes"
|
||||
|
||||
# The Items
|
||||
item_1 = "Item 1 - Propeller"
|
||||
item_2 = "Item 2 - Rocket"
|
||||
item_3 = "Item 3 - Bouncy"
|
||||
|
||||
# Misc. Items
|
||||
one_up = "1-Up"
|
||||
weapon_energy = "Weapon Energy (L)"
|
||||
health_energy = "Health Energy (L)"
|
||||
e_tank = "E-Tank"
|
||||
|
||||
# Locations
|
||||
crash_man = "Crash Man - Defeated"
|
||||
metal_man = "Metal Man - Defeated"
|
||||
quick_man = "Quick Man - Defeated"
|
||||
bubble_man = "Bubble Man - Defeated"
|
||||
heat_man = "Heat Man - Defeated"
|
||||
wood_man = "Wood Man - Defeated"
|
||||
flash_man = "Flash Man - Defeated"
|
||||
air_man = "Air Man - Defeated"
|
||||
crash_bomber_get = "Crash Bomber - Received"
|
||||
metal_blade_get = "Metal Blade - Received"
|
||||
quick_boomerang_get = "Quick Boomerang - Received"
|
||||
bubble_lead_get = "Bubble Lead - Received"
|
||||
atomic_fire_get = "Atomic Fire - Received"
|
||||
leaf_shield_get = "Leaf Shield - Received"
|
||||
time_stopper_get = "Time Stopper - Received"
|
||||
air_shooter_get = "Air Shooter - Received"
|
||||
item_1_get = "Item 1 - Received"
|
||||
item_2_get = "Item 2 - Received"
|
||||
item_3_get = "Item 3 - Received"
|
||||
wily_1 = "Mecha Dragon - Defeated"
|
||||
wily_2 = "Picopico-kun - Defeated"
|
||||
wily_3 = "Guts Tank - Defeated"
|
||||
wily_4 = "Boobeam Trap - Defeated"
|
||||
wily_5 = "Wily Machine 2 - Defeated"
|
||||
dr_wily = "Dr. Wily (Alien) - Defeated"
|
||||
|
||||
# Wily Stage Event Items
|
||||
wily_stage_1 = "Wily Stage 1 - Completed"
|
||||
wily_stage_2 = "Wily Stage 2 - Completed"
|
||||
wily_stage_3 = "Wily Stage 3 - Completed"
|
||||
wily_stage_4 = "Wily Stage 4 - Completed"
|
||||
wily_stage_5 = "Wily Stage 5 - Completed"
|
||||
|
||||
# Consumable Locations
|
||||
heat_man_c1 = "Heat Man Stage - 1-Up" # 3, requires Yoku jumps or Item 2
|
||||
flash_man_c1 = "Flash Man Stage - Health Energy 1" # 0
|
||||
flash_man_c2 = "Flash Man Stage - 1-Up" # 2, requires any Item
|
||||
flash_man_c3 = "Flash Man Stage - Health Energy 2" # 6, requires Crash Bomber
|
||||
flash_man_c4 = "Flash Man Stage - Weapon Energy 1" # 8, requires Crash Bomber
|
||||
flash_man_c5 = "Flash Man Stage - Health Energy 3" # 9
|
||||
flash_man_c6 = "Flash Man Stage - E-Tank" # 10
|
||||
quick_man_c1 = "Quick Man Stage - 1-Up 1" # 0, needs any Item
|
||||
quick_man_c2 = "Quick Man Stage - E-Tank" # 1, requires allow lasers or Time Stopper
|
||||
quick_man_c3 = "Quick Man Stage - 1-Up 2" # 2, requires allow lasers or Time Stopper
|
||||
quick_man_c4 = "Quick Man Stage - Weapon Energy 1" # 3, requires allow lasers or Time Stopper
|
||||
quick_man_c5 = "Quick Man Stage - Weapon Energy 2" # 4, requires allow lasers or Time Stopper
|
||||
quick_man_c6 = "Quick Man Stage - Health Energy" # 5, requires allow lasers or Time Stopper
|
||||
quick_man_c7 = "Quick Man Stage - 1-Up 3" # 6, requires allow lasers or Time Stopper
|
||||
quick_man_c8 = "Quick Man Stage - Weapon Energy 3" # 7, requires allow lasers or Time Stopper
|
||||
metal_man_c1 = "Metal Man Stage - E-Tank 1" # 0
|
||||
metal_man_c2 = "Metal Man Stage - 1-Up" # 1, needs Item 1/2
|
||||
metal_man_c3 = "Metal Man Stage - E-Tank 2" # 2, needs Item 1/2 (without putting dying in logic at least)
|
||||
crash_man_c1 = "Crash Man Stage - Health Energy" # 0
|
||||
crash_man_c2 = "Crash Man Stage - E-Tank" # 1
|
||||
crash_man_c3 = "Crash Man Stage - 1-Up" # 2, any Item
|
||||
wily_1_c1 = "Wily Stage 1 - 1-Up" # 10
|
||||
wily_1_c2 = "Wily Stage 1 - Weapon Energy 1" # 11
|
||||
wily_2_c1 = "Wily Stage 2 - Weapon Energy 1" # 11
|
||||
wily_2_c2 = "Wily Stage 2 - Weapon Energy 2" # 12
|
||||
wily_2_c3 = "Wily Stage 2 - E-Tank 1" # 16
|
||||
wily_2_c4 = "Wily Stage 2 - 1-Up 1" # 17
|
||||
# 18 - 27 are all small weapon energies, might force these local junk?
|
||||
wily_2_c8 = "Wily Stage 2 - Weapon Energy 3" # 18
|
||||
wily_2_c9 = "Wily Stage 2 - Weapon Energy 4" # 19
|
||||
wily_2_c10 = "Wily Stage 2 - Weapon Energy 5" # 20
|
||||
wily_2_c11 = "Wily Stage 2 - Weapon Energy 6" # 21
|
||||
wily_2_c12 = "Wily Stage 2 - Weapon Energy 7" # 22
|
||||
wily_2_c13 = "Wily Stage 2 - Weapon Energy 8" # 23
|
||||
wily_2_c14 = "Wily Stage 2 - Weapon Energy 9" # 24
|
||||
wily_2_c15 = "Wily Stage 2 - Weapon Energy 10" # 25
|
||||
wily_2_c16 = "Wily Stage 2 - Weapon Energy 11" # 26
|
||||
wily_2_c5 = "Wily Stage 2 - 1-Up 2" # 29, requires Crash Bomber
|
||||
wily_2_c6 = "Wily Stage 2 - E-Tank 2" # 30, requires Crash Bomber
|
||||
wily_2_c7 = "Wily Stage 2 - Health Energy" # 31, item 2 (already required to reach wily 2)
|
||||
wily_3_c1 = "Wily Stage 3 - Weapon Energy 1" # 12, requires Crash Bomber
|
||||
wily_3_c2 = "Wily Stage 3 - E-Tank" # 17, requires Crash Bomber
|
||||
wily_3_c3 = "Wily Stage 3 - Weapon Energy 2" # 18
|
||||
wily_3_c4 = "Wily Stage 3 - Weapon Energy 3" # 19
|
||||
wily_4_c1 = "Wily Stage 4 - Weapon Energy 1" # 16
|
||||
wily_4_c2 = "Wily Stage 4 - Weapon Energy 2" # 17
|
||||
wily_4_c3 = "Wily Stage 4 - 1-Up 1" # 18
|
||||
wily_4_c4 = "Wily Stage 4 - E-Tank 1" # 19
|
||||
229
worlds/mm2/options.py
Normal file
229
worlds/mm2/options.py
Normal file
@@ -0,0 +1,229 @@
|
||||
from dataclasses import dataclass
|
||||
|
||||
from Options import Choice, Toggle, DeathLink, DefaultOnToggle, TextChoice, Range, OptionDict, PerGameCommonOptions
|
||||
from schema import Schema, And, Use, Optional
|
||||
|
||||
bosses = {
|
||||
"Heat Man": 0,
|
||||
"Air Man": 1,
|
||||
"Wood Man": 2,
|
||||
"Bubble Man": 3,
|
||||
"Quick Man": 4,
|
||||
"Flash Man": 5,
|
||||
"Metal Man": 6,
|
||||
"Crash Man": 7,
|
||||
"Mecha Dragon": 8,
|
||||
"Picopico-kun": 9,
|
||||
"Guts Tank": 10,
|
||||
"Boobeam Trap": 11,
|
||||
"Wily Machine 2": 12,
|
||||
"Alien": 13
|
||||
}
|
||||
|
||||
weapons_to_id = {
|
||||
"Mega Buster": 0,
|
||||
"Atomic Fire": 1,
|
||||
"Air Shooter": 2,
|
||||
"Leaf Shield": 3,
|
||||
"Bubble Lead": 4,
|
||||
"Quick Boomerang": 5,
|
||||
"Metal Blade": 7,
|
||||
"Crash Bomber": 6,
|
||||
"Time Stopper": 8,
|
||||
}
|
||||
|
||||
|
||||
class EnergyLink(Toggle):
|
||||
"""
|
||||
Enables EnergyLink support.
|
||||
When enabled, pickups dropped from enemies are sent to the EnergyLink pool, and healing/weapon energy/1-Ups can
|
||||
be requested from the EnergyLink pool.
|
||||
Some of the energy sent to the pool will be lost on transfer.
|
||||
"""
|
||||
display_name = "EnergyLink"
|
||||
|
||||
|
||||
class StartingRobotMaster(Choice):
|
||||
"""
|
||||
The initial stage unlocked at the start.
|
||||
"""
|
||||
display_name = "Starting Robot Master"
|
||||
option_heat_man = 0
|
||||
option_air_man = 1
|
||||
option_wood_man = 2
|
||||
option_bubble_man = 3
|
||||
option_quick_man = 4
|
||||
option_flash_man = 5
|
||||
option_metal_man = 6
|
||||
option_crash_man = 7
|
||||
default = "random"
|
||||
|
||||
|
||||
class YokuJumps(Toggle):
|
||||
"""
|
||||
When enabled, the player is expected to be able to perform the yoku block sequence in Heat Man's
|
||||
stage without Item 2.
|
||||
"""
|
||||
display_name = "Yoku Block Jumps"
|
||||
|
||||
|
||||
class EnableLasers(Toggle):
|
||||
"""
|
||||
When enabled, the player is expected to complete (and acquire items within) the laser sections of Quick Man's
|
||||
stage without the Time Stopper.
|
||||
"""
|
||||
display_name = "Enable Lasers"
|
||||
|
||||
|
||||
class Consumables(Choice):
|
||||
"""
|
||||
When enabled, e-tanks/1-ups/health/weapon energy will be added to the pool of items and included as checks.
|
||||
E-Tanks and 1-Ups add 20 checks to the pool.
|
||||
Weapon/Health Energy add 27 checks to the pool.
|
||||
"""
|
||||
display_name = "Consumables"
|
||||
option_none = 0
|
||||
option_1up_etank = 1
|
||||
option_weapon_health = 2
|
||||
option_all = 3
|
||||
default = 1
|
||||
alias_true = 3
|
||||
alias_false = 0
|
||||
|
||||
@classmethod
|
||||
def get_option_name(cls, value: int) -> str:
|
||||
if value == 1:
|
||||
return "1-Ups/E-Tanks"
|
||||
if value == 2:
|
||||
return "Weapon/Health Energy"
|
||||
return super().get_option_name(value)
|
||||
|
||||
|
||||
class Quickswap(DefaultOnToggle):
|
||||
"""
|
||||
When enabled, the player can quickswap through all received weapons by pressing Select.
|
||||
"""
|
||||
display_name = "Quickswap"
|
||||
|
||||
|
||||
class PaletteShuffle(TextChoice):
|
||||
"""
|
||||
Change the color of Mega Man and the Robot Masters.
|
||||
None: The palettes are unchanged.
|
||||
Shuffled: Palette colors are shuffled amongst the robot masters.
|
||||
Randomized: Random (usually good) palettes are generated for each robot master.
|
||||
Singularity: one palette is generated and used for all robot masters.
|
||||
Supports custom palettes using HTML named colors in the
|
||||
following format: Mega Buster-Lavender|Violet;randomized
|
||||
The first value is the character whose palette you'd like to define, then separated by - is a set of 2 colors for
|
||||
that character. separate every color with a pipe, and separate every character as well as the remaining shuffle with
|
||||
a semicolon.
|
||||
"""
|
||||
display_name = "Palette Shuffle"
|
||||
option_none = 0
|
||||
option_shuffled = 1
|
||||
option_randomized = 2
|
||||
option_singularity = 3
|
||||
|
||||
|
||||
class EnemyWeaknesses(Toggle):
|
||||
"""
|
||||
Randomizes the damage dealt to enemies by weapons. Friender will always take damage from the buster.
|
||||
"""
|
||||
display_name = "Random Enemy Weaknesses"
|
||||
|
||||
|
||||
class StrictWeaknesses(Toggle):
|
||||
"""
|
||||
Only your starting Robot Master will take damage from the Mega Buster, the rest must be defeated with weapons.
|
||||
Weapons that only do 1-3 damage to bosses no longer deal damage (aside from Alien).
|
||||
"""
|
||||
display_name = "Strict Boss Weaknesses"
|
||||
|
||||
|
||||
class RandomWeaknesses(Choice):
|
||||
"""
|
||||
None: Bosses will have their regular weaknesses.
|
||||
Shuffled: Weapon damage will be shuffled amongst the weapons, so Metal Blade may do Bubble Lead damage.
|
||||
Time Stopper will deplete half of a random Robot Master's HP.
|
||||
Randomized: Weapon damage will be fully randomized.
|
||||
"""
|
||||
display_name = "Random Boss Weaknesses"
|
||||
option_none = 0
|
||||
option_shuffled = 1
|
||||
option_randomized = 2
|
||||
alias_false = 0
|
||||
alias_true = 2
|
||||
|
||||
|
||||
class Wily5Requirement(Range):
|
||||
"""Change the number of Robot Masters that are required to be defeated for
|
||||
the teleporter to the Wily Machine to appear."""
|
||||
display_name = "Wily 5 Requirement"
|
||||
default = 8
|
||||
range_start = 1
|
||||
range_end = 8
|
||||
|
||||
|
||||
class WeaknessPlando(OptionDict):
|
||||
"""
|
||||
Specify specific damage numbers for boss damage. Can be used even without strict/random weaknesses.
|
||||
plando_weakness:
|
||||
Robot Master:
|
||||
Weapon: Damage
|
||||
"""
|
||||
display_name = "Plando Weaknesses"
|
||||
schema = Schema({
|
||||
Optional(And(str, Use(str.title), lambda s: s in bosses)): {
|
||||
And(str, Use(str.title), lambda s: s in weapons_to_id): And(int, lambda i: i in range(-1, 14))
|
||||
}
|
||||
})
|
||||
default = {}
|
||||
|
||||
|
||||
class ReduceFlashing(Choice):
|
||||
"""
|
||||
Reduce flashing seen in gameplay, such as the stage select and when defeating a Wily boss.
|
||||
Virtual Console: increases length of most flashes, changes some flashes from white to a dark gray.
|
||||
Minor: VC changes + decreasing the speed of Bubble/Metal Man stage animations.
|
||||
Full: VC changes + further decreasing the brightness of most flashes and
|
||||
disables stage animations for Metal/Bubble Man stages.
|
||||
"""
|
||||
display_name = "Reduce Flashing"
|
||||
option_none = 0
|
||||
option_virtual_console = 1
|
||||
option_minor = 2
|
||||
option_full = 3
|
||||
default = 1
|
||||
|
||||
|
||||
class RandomMusic(Choice):
|
||||
"""
|
||||
Vanilla: music is unchanged
|
||||
Shuffled: stage and certain menu music is shuffled.
|
||||
Randomized: stage and certain menu music is randomly selected
|
||||
None: no music will play
|
||||
"""
|
||||
display_name = "Random Music"
|
||||
option_vanilla = 0
|
||||
option_shuffled = 1
|
||||
option_randomized = 2
|
||||
option_none = 3
|
||||
|
||||
@dataclass
|
||||
class MM2Options(PerGameCommonOptions):
|
||||
death_link: DeathLink
|
||||
energy_link: EnergyLink
|
||||
starting_robot_master: StartingRobotMaster
|
||||
consumables: Consumables
|
||||
yoku_jumps: YokuJumps
|
||||
enable_lasers: EnableLasers
|
||||
enemy_weakness: EnemyWeaknesses
|
||||
strict_weakness: StrictWeaknesses
|
||||
random_weakness: RandomWeaknesses
|
||||
wily_5_requirement: Wily5Requirement
|
||||
plando_weakness: WeaknessPlando
|
||||
palette_shuffle: PaletteShuffle
|
||||
quickswap: Quickswap
|
||||
reduce_flashing: ReduceFlashing
|
||||
random_music: RandomMusic
|
||||
415
worlds/mm2/rom.py
Normal file
415
worlds/mm2/rom.py
Normal file
@@ -0,0 +1,415 @@
|
||||
import pkgutil
|
||||
from typing import Optional, TYPE_CHECKING, Iterable, Dict, Sequence
|
||||
import hashlib
|
||||
import Utils
|
||||
import os
|
||||
|
||||
import settings
|
||||
from worlds.Files import APProcedurePatch, APTokenMixin, APTokenTypes
|
||||
from . import names
|
||||
from .rules import minimum_weakness_requirement
|
||||
from .text import MM2TextEntry
|
||||
from .color import get_colors_for_item, write_palette_shuffle
|
||||
from .options import Consumables, ReduceFlashing, RandomMusic
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import MM2World
|
||||
|
||||
MM2LCHASH = "37f2c36ce7592f1e16b3434b3985c497"
|
||||
PROTEUSHASH = "9ff045a3ca30018b6e874c749abb3ec4"
|
||||
MM2NESHASH = "0527a0ee512f69e08b8db6dc97964632"
|
||||
MM2VCHASH = "0c78dfe8e90fb8f3eed022ff01126ad3"
|
||||
|
||||
enemy_weakness_ptrs: Dict[int, int] = {
|
||||
0: 0x3E9A8,
|
||||
1: 0x3EA24,
|
||||
2: 0x3EA9C,
|
||||
3: 0x3EB14,
|
||||
4: 0x3EB8C,
|
||||
5: 0x3EC04,
|
||||
6: 0x3EC7C,
|
||||
7: 0x3ECF4,
|
||||
}
|
||||
|
||||
enemy_addresses: Dict[str, int] = {
|
||||
"Shrink": 0x00,
|
||||
"M-445": 0x04,
|
||||
"Claw": 0x08,
|
||||
"Tanishi": 0x0A,
|
||||
"Kerog": 0x0C,
|
||||
"Petit Kerog": 0x0D,
|
||||
"Anko": 0x0F,
|
||||
"Batton": 0x16,
|
||||
"Robitto": 0x17,
|
||||
"Friender": 0x1C,
|
||||
"Monking": 0x1D,
|
||||
"Kukku": 0x1F,
|
||||
"Telly": 0x22,
|
||||
"Changkey Maker": 0x23,
|
||||
"Changkey": 0x24,
|
||||
"Pierrobot": 0x29,
|
||||
"Fly Boy": 0x2C,
|
||||
# "Crash Wall": 0x2D
|
||||
# "Friender Wall": 0x2E
|
||||
"Blocky": 0x31,
|
||||
"Neo Metall": 0x34,
|
||||
"Matasaburo": 0x36,
|
||||
"Pipi": 0x38,
|
||||
"Pipi Egg": 0x3A,
|
||||
"Copipi": 0x3C,
|
||||
"Kaminari Goro": 0x3E,
|
||||
"Petit Goblin": 0x45,
|
||||
"Springer": 0x46,
|
||||
"Mole (Up)": 0x48,
|
||||
"Mole (Down)": 0x49,
|
||||
"Shotman (Left)": 0x4B,
|
||||
"Shotman (Right)": 0x4C,
|
||||
"Sniper Armor": 0x4E,
|
||||
"Sniper Joe": 0x4F,
|
||||
"Scworm": 0x50,
|
||||
"Scworm Worm": 0x51,
|
||||
"Picopico-kun": 0x6A,
|
||||
"Boobeam Trap": 0x6D,
|
||||
"Big Fish": 0x71
|
||||
}
|
||||
|
||||
# addresses printed when assembling basepatch
|
||||
consumables_ptr: int = 0x3F2FE
|
||||
quickswap_ptr: int = 0x3F363
|
||||
wily_5_ptr: int = 0x3F3A1
|
||||
energylink_ptr: int = 0x3F46B
|
||||
get_equipped_sound_ptr: int = 0x3F384
|
||||
|
||||
|
||||
class RomData:
|
||||
def __init__(self, file: bytes, name: str = "") -> None:
|
||||
self.file = bytearray(file)
|
||||
self.name = name
|
||||
|
||||
def read_byte(self, offset: int) -> int:
|
||||
return self.file[offset]
|
||||
|
||||
def read_bytes(self, offset: int, length: int) -> bytearray:
|
||||
return self.file[offset:offset + length]
|
||||
|
||||
def write_byte(self, offset: int, value: int) -> None:
|
||||
self.file[offset] = value
|
||||
|
||||
def write_bytes(self, offset: int, values: Sequence[int]) -> None:
|
||||
self.file[offset:offset + len(values)] = values
|
||||
|
||||
def write_to_file(self, file: str) -> None:
|
||||
with open(file, 'wb') as outfile:
|
||||
outfile.write(self.file)
|
||||
|
||||
|
||||
class MM2ProcedurePatch(APProcedurePatch, APTokenMixin):
|
||||
hash = [MM2LCHASH, MM2NESHASH, MM2VCHASH]
|
||||
game = "Mega Man 2"
|
||||
patch_file_ending = ".apmm2"
|
||||
result_file_ending = ".nes"
|
||||
name: bytearray
|
||||
procedure = [
|
||||
("apply_bsdiff4", ["mm2_basepatch.bsdiff4"]),
|
||||
("apply_tokens", ["token_patch.bin"]),
|
||||
]
|
||||
|
||||
@classmethod
|
||||
def get_source_data(cls) -> bytes:
|
||||
return get_base_rom_bytes()
|
||||
|
||||
def write_byte(self, offset: int, value: int) -> None:
|
||||
self.write_token(APTokenTypes.WRITE, offset, value.to_bytes(1, "little"))
|
||||
|
||||
def write_bytes(self, offset: int, value: Iterable[int]) -> None:
|
||||
self.write_token(APTokenTypes.WRITE, offset, bytes(value))
|
||||
|
||||
|
||||
def patch_rom(world: "MM2World", patch: MM2ProcedurePatch) -> None:
|
||||
patch.write_file("mm2_basepatch.bsdiff4", pkgutil.get_data(__name__, os.path.join("data", "mm2_basepatch.bsdiff4")))
|
||||
# text writing
|
||||
patch.write_bytes(0x37E2A, MM2TextEntry("FOR ", 0xCB).resolve())
|
||||
patch.write_bytes(0x37EAA, MM2TextEntry("GET EQUIPPED ", 0x0B).resolve())
|
||||
patch.write_bytes(0x37EBA, MM2TextEntry("WITH ", 0x2B).resolve())
|
||||
|
||||
base_address = 0x3F650
|
||||
color_address = 0x37F6C
|
||||
for i, location in zip(range(11), [
|
||||
names.atomic_fire_get,
|
||||
names.air_shooter_get,
|
||||
names.leaf_shield_get,
|
||||
names.bubble_lead_get,
|
||||
names.quick_boomerang_get,
|
||||
names.time_stopper_get,
|
||||
names.metal_blade_get,
|
||||
names.crash_bomber_get,
|
||||
names.item_1_get,
|
||||
names.item_2_get,
|
||||
names.item_3_get
|
||||
]):
|
||||
item = world.multiworld.get_location(location, world.player).item
|
||||
if item:
|
||||
if len(item.name) <= 14:
|
||||
# we want to just place it in the center
|
||||
first_str = ""
|
||||
second_str = item.name
|
||||
third_str = ""
|
||||
elif len(item.name) <= 28:
|
||||
# spread across second and third
|
||||
first_str = ""
|
||||
second_str = item.name[:14]
|
||||
third_str = item.name[14:]
|
||||
else:
|
||||
# all three
|
||||
first_str = item.name[:14]
|
||||
second_str = item.name[14:28]
|
||||
third_str = item.name[28:]
|
||||
if len(third_str) > 16:
|
||||
third_str = third_str[:16]
|
||||
player_str = world.multiworld.get_player_name(item.player)
|
||||
if len(player_str) > 14:
|
||||
player_str = player_str[:14]
|
||||
patch.write_bytes(base_address + (64 * i), MM2TextEntry(first_str, 0x4B).resolve())
|
||||
patch.write_bytes(base_address + (64 * i) + 16, MM2TextEntry(second_str, 0x6B).resolve())
|
||||
patch.write_bytes(base_address + (64 * i) + 32, MM2TextEntry(third_str, 0x8B).resolve())
|
||||
patch.write_bytes(base_address + (64 * i) + 48, MM2TextEntry(player_str, 0xEB).resolve())
|
||||
|
||||
colors = get_colors_for_item(item.name)
|
||||
if i > 7:
|
||||
patch.write_bytes(color_address + 27 + ((i - 8) * 2), colors)
|
||||
else:
|
||||
patch.write_bytes(color_address + (i * 2), colors)
|
||||
|
||||
write_palette_shuffle(world, patch)
|
||||
|
||||
enemy_weaknesses: Dict[str, Dict[int, int]] = {}
|
||||
|
||||
if world.options.strict_weakness or world.options.random_weakness or world.options.plando_weakness:
|
||||
# we need to write boss weaknesses
|
||||
output = bytearray()
|
||||
for weapon in world.weapon_damage:
|
||||
if weapon == 8:
|
||||
continue # Time Stopper is a special case
|
||||
weapon_damage = [world.weapon_damage[weapon][i]
|
||||
if world.weapon_damage[weapon][i] >= 0
|
||||
else 256 + world.weapon_damage[weapon][i]
|
||||
for i in range(14)]
|
||||
output.extend(weapon_damage)
|
||||
patch.write_bytes(0x2E952, bytes(output))
|
||||
time_stopper_damage = world.weapon_damage[8]
|
||||
time_offset = 0x2C03B
|
||||
damage_table = {
|
||||
4: 0xF,
|
||||
3: 0x17,
|
||||
2: 0x1E,
|
||||
1: 0x25
|
||||
}
|
||||
for boss, damage in enumerate(time_stopper_damage):
|
||||
if damage > 4:
|
||||
damage = 4 # 4 is a guaranteed kill, no need to exceed
|
||||
if damage <= 0:
|
||||
patch.write_byte(time_offset + 14 + boss, 0)
|
||||
else:
|
||||
patch.write_byte(time_offset + 14 + boss, 1)
|
||||
patch.write_byte(time_offset + boss, damage_table[damage])
|
||||
if world.options.random_weakness:
|
||||
wily_5_weaknesses = [i for i in range(8) if world.weapon_damage[i][12] > minimum_weakness_requirement[i]]
|
||||
world.random.shuffle(wily_5_weaknesses)
|
||||
if len(wily_5_weaknesses) >= 3:
|
||||
weak1 = wily_5_weaknesses.pop()
|
||||
weak2 = wily_5_weaknesses.pop()
|
||||
weak3 = wily_5_weaknesses.pop()
|
||||
elif len(wily_5_weaknesses) == 2:
|
||||
weak1 = weak2 = wily_5_weaknesses.pop()
|
||||
weak3 = wily_5_weaknesses.pop()
|
||||
else:
|
||||
weak1 = weak2 = weak3 = 0
|
||||
patch.write_byte(0x2DA2E, weak1)
|
||||
patch.write_byte(0x2DA32, weak2)
|
||||
patch.write_byte(0x2DA3A, weak3)
|
||||
enemy_weaknesses["Picopico-kun"] = {weapon: world.weapon_damage[weapon][9] for weapon in range(8)}
|
||||
enemy_weaknesses["Boobeam Trap"] = {weapon: world.weapon_damage[weapon][11] for weapon in range(8)}
|
||||
|
||||
if world.options.enemy_weakness:
|
||||
for enemy in enemy_addresses:
|
||||
if enemy in ("Picopico-kun", "Boobeam Trap"):
|
||||
continue
|
||||
enemy_weaknesses[enemy] = {weapon: world.random.randint(-4, 4) for weapon in enemy_weakness_ptrs}
|
||||
if enemy == "Friender":
|
||||
# Friender has to be killed, need buster damage to not break logic
|
||||
enemy_weaknesses[enemy][0] = max(enemy_weaknesses[enemy][0], 1)
|
||||
|
||||
for enemy, damage_table in enemy_weaknesses.items():
|
||||
for weapon in enemy_weakness_ptrs:
|
||||
if damage_table[weapon] < 0:
|
||||
damage_table[weapon] = 256 + damage_table[weapon]
|
||||
patch.write_byte(enemy_weakness_ptrs[weapon] + enemy_addresses[enemy], damage_table[weapon])
|
||||
|
||||
if world.options.quickswap:
|
||||
patch.write_byte(quickswap_ptr + 1, 0x01)
|
||||
|
||||
if world.options.consumables != Consumables.option_all:
|
||||
value_a = 0x7C
|
||||
value_b = 0x76
|
||||
if world.options.consumables == Consumables.option_1up_etank:
|
||||
value_b = 0x7A
|
||||
else:
|
||||
value_a = 0x7A
|
||||
patch.write_byte(consumables_ptr - 3, value_a)
|
||||
patch.write_byte(consumables_ptr + 1, value_b)
|
||||
|
||||
patch.write_byte(wily_5_ptr + 1, world.options.wily_5_requirement.value)
|
||||
|
||||
if world.options.energy_link:
|
||||
patch.write_byte(energylink_ptr + 1, 1)
|
||||
|
||||
if world.options.reduce_flashing:
|
||||
if world.options.reduce_flashing.value == ReduceFlashing.option_virtual_console:
|
||||
color = 0x2D # Dark Gray
|
||||
speed = -1
|
||||
elif world.options.reduce_flashing.value == ReduceFlashing.option_minor:
|
||||
color = 0x2D
|
||||
speed = 0x08
|
||||
else:
|
||||
color = 0x0F
|
||||
speed = 0x00
|
||||
patch.write_byte(0x2D1B0, color) # Change white to a dark gray, Mecha Dragon
|
||||
patch.write_byte(0x2D397, 0x0F) # Longer flash time, Mecha Dragon kill
|
||||
patch.write_byte(0x2D3A0, color) # Change white to a dark gray, Picopico-kun/Boobeam Trap
|
||||
patch.write_byte(0x2D65F, color) # Change white to a dark gray, Guts Tank
|
||||
patch.write_byte(0x2DA94, color) # Change white to a dark gray, Wily Machine
|
||||
patch.write_byte(0x2DC97, color) # Change white to a dark gray, Alien
|
||||
patch.write_byte(0x2DD68, 0x10) # Longer flash time, Alien kill
|
||||
patch.write_bytes(0x2DF14, [0xEA, 0xEA, 0xEA, 0xEA, 0xEA, 0xEA]) # Reduce final Alien flash to 1 big flash
|
||||
patch.write_byte(0x34132, 0x08) # Longer flash time, Stage Select
|
||||
|
||||
if world.options.reduce_flashing.value == ReduceFlashing.option_full:
|
||||
# reduce color of stage flashing
|
||||
patch.write_bytes(0x344C9, [0x2D, 0x10, 0x00, 0x2D,
|
||||
0x0F, 0x10, 0x2D, 0x00,
|
||||
0x0F, 0x10, 0x2D, 0x00,
|
||||
0x0F, 0x10, 0x2D, 0x00,
|
||||
0x2D, 0x10, 0x2D, 0x00,
|
||||
0x0F, 0x10, 0x2D, 0x00,
|
||||
0x0F, 0x10, 0x2D, 0x00,
|
||||
0x0F, 0x10, 0x2D, 0x00])
|
||||
# remove wily castle flash
|
||||
patch.write_byte(0x3596D, 0x0F)
|
||||
|
||||
if speed != -1:
|
||||
patch.write_byte(0xFE01, speed) # Bubble Man Stage
|
||||
patch.write_byte(0x1BE01, speed) # Metal Man Stage
|
||||
|
||||
if world.options.random_music:
|
||||
if world.options.random_music == RandomMusic.option_none:
|
||||
pool = [0xFF] * 20
|
||||
# A couple of additional mutes we want here
|
||||
patch.write_byte(0x37819, 0xFF) # Credits
|
||||
patch.write_byte(0x378A4, 0xFF) # Credits #2
|
||||
patch.write_byte(0x37149, 0xFF) # Game Over Jingle
|
||||
patch.write_byte(0x341BA, 0xFF) # Robot Master Jingle
|
||||
patch.write_byte(0x2E0B4, 0xFF) # Robot Master Defeated
|
||||
patch.write_byte(0x35B78, 0xFF) # Wily Castle
|
||||
patch.write_byte(0x2DFA5, 0xFF) # Wily Defeated
|
||||
|
||||
elif world.options.random_music == RandomMusic.option_shuffled:
|
||||
pool = [0, 1, 2, 3, 4, 5, 6, 7, 8, 8, 9, 9, 9, 0x10, 0xC, 0xB, 0x17, 0x13, 0xE, 0xD]
|
||||
world.random.shuffle(pool)
|
||||
else:
|
||||
pool = world.random.choices([0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 0xB, 0xC, 0xD, 0xE, 0x10, 0x13, 0x17], k=20)
|
||||
patch.write_bytes(0x381E0, pool[:13])
|
||||
patch.write_byte(0x36318, pool[13]) # Game Start
|
||||
patch.write_byte(0x37181, pool[13]) # Game Over
|
||||
patch.write_byte(0x340AE, pool[14]) # RBM Select
|
||||
patch.write_byte(0x39005, pool[15]) # Robot Master Battle
|
||||
patch.write_byte(get_equipped_sound_ptr + 1, pool[16]) # Get Equipped, we actually hook this already lmao
|
||||
patch.write_byte(0x3775A, pool[17]) # Epilogue
|
||||
patch.write_byte(0x36089, pool[18]) # Intro
|
||||
patch.write_byte(0x361F1, pool[19]) # Title
|
||||
|
||||
|
||||
|
||||
from Utils import __version__
|
||||
patch.name = bytearray(f'MM2{__version__.replace(".", "")[0:3]}_{world.player}_{world.multiworld.seed:11}\0',
|
||||
'utf8')[:21]
|
||||
patch.name.extend([0] * (21 - len(patch.name)))
|
||||
patch.write_bytes(0x3FFC0, patch.name)
|
||||
deathlink_byte = world.options.death_link.value | (world.options.energy_link.value << 1)
|
||||
patch.write_byte(0x3FFD5, deathlink_byte)
|
||||
|
||||
patch.write_bytes(0x3FFD8, world.world_version)
|
||||
|
||||
version_map = {
|
||||
"0": 0x90,
|
||||
"1": 0x91,
|
||||
"2": 0x92,
|
||||
"3": 0x93,
|
||||
"4": 0x94,
|
||||
"5": 0x95,
|
||||
"6": 0x96,
|
||||
"7": 0x97,
|
||||
"8": 0x98,
|
||||
"9": 0x99,
|
||||
".": 0xDC
|
||||
}
|
||||
patch.write_token(APTokenTypes.RLE, 0x36EE0, (11, 0))
|
||||
patch.write_token(APTokenTypes.RLE, 0x36EEE, (25, 0))
|
||||
|
||||
# BY SILVRIS
|
||||
patch.write_bytes(0x36EE0, [0xC2, 0xD9, 0xC0, 0xD3, 0xC9, 0xCC, 0xD6, 0xD2, 0xC9, 0xD3])
|
||||
# ARCHIPELAGO x.x.x
|
||||
patch.write_bytes(0x36EF2, [0xC1, 0xD2, 0xC3, 0xC8, 0xC9, 0xD0, 0xC5, 0xCC, 0xC1, 0xC7, 0xCF, 0xC0])
|
||||
patch.write_bytes(0x36EFE, list(map(lambda c: version_map[c], __version__)))
|
||||
|
||||
patch.write_file("token_patch.bin", patch.get_token_binary())
|
||||
|
||||
|
||||
header = b"\x4E\x45\x53\x1A\x10\x00\x10\x00\x00\x00\x00\x00\x00\x00\x00\x00"
|
||||
|
||||
|
||||
def read_headerless_nes_rom(rom: bytes) -> bytes:
|
||||
if rom[:4] == b"NES\x1A":
|
||||
return rom[16:]
|
||||
else:
|
||||
return rom
|
||||
|
||||
|
||||
def get_base_rom_bytes(file_name: str = "") -> bytes:
|
||||
base_rom_bytes: Optional[bytes] = getattr(get_base_rom_bytes, "base_rom_bytes", None)
|
||||
if not base_rom_bytes:
|
||||
file_name = get_base_rom_path(file_name)
|
||||
base_rom_bytes = read_headerless_nes_rom(bytes(open(file_name, "rb").read()))
|
||||
|
||||
basemd5 = hashlib.md5()
|
||||
basemd5.update(base_rom_bytes)
|
||||
if basemd5.hexdigest() == PROTEUSHASH:
|
||||
base_rom_bytes = extract_mm2(base_rom_bytes)
|
||||
basemd5 = hashlib.md5()
|
||||
basemd5.update(base_rom_bytes)
|
||||
if basemd5.hexdigest() not in {MM2LCHASH, MM2NESHASH, MM2VCHASH}:
|
||||
print(basemd5.hexdigest())
|
||||
raise Exception("Supplied Base Rom does not match known MD5 for US, LC, or US VC release. "
|
||||
"Get the correct game and version, then dump it")
|
||||
headered_rom = bytearray(base_rom_bytes)
|
||||
headered_rom[0:0] = header
|
||||
setattr(get_base_rom_bytes, "base_rom_bytes", bytes(headered_rom))
|
||||
return bytes(headered_rom)
|
||||
return base_rom_bytes
|
||||
|
||||
|
||||
def get_base_rom_path(file_name: str = "") -> str:
|
||||
options: settings.Settings = settings.get_settings()
|
||||
if not file_name:
|
||||
file_name = options["mm2_options"]["rom_file"]
|
||||
if not os.path.exists(file_name):
|
||||
file_name = Utils.user_path(file_name)
|
||||
return file_name
|
||||
|
||||
|
||||
PRG_OFFSET = 0x8ED70
|
||||
PRG_SIZE = 0x40000
|
||||
|
||||
|
||||
def extract_mm2(proteus: bytes) -> bytes:
|
||||
mm2 = bytearray(proteus[PRG_OFFSET:PRG_OFFSET + PRG_SIZE])
|
||||
return bytes(mm2)
|
||||
321
worlds/mm2/rules.py
Normal file
321
worlds/mm2/rules.py
Normal file
@@ -0,0 +1,321 @@
|
||||
from math import ceil
|
||||
from typing import TYPE_CHECKING, Dict, List
|
||||
from . import names
|
||||
from .locations import heat_man_locations, air_man_locations, wood_man_locations, bubble_man_locations, \
|
||||
quick_man_locations, flash_man_locations, metal_man_locations, crash_man_locations, wily_1_locations, \
|
||||
wily_2_locations, wily_3_locations, wily_4_locations, wily_5_locations, wily_6_locations
|
||||
from .options import bosses, weapons_to_id, Consumables, RandomWeaknesses
|
||||
from worlds.generic.Rules import add_rule
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from . import MM2World
|
||||
from BaseClasses import CollectionState
|
||||
|
||||
weapon_damage: Dict[int, List[int]] = {
|
||||
0: [2, 2, 1, 1, 2, 2, 1, 1, 1, 7, 1, 0, 1, -1], # Mega Buster
|
||||
1: [-1, 6, 0xE, 0, 0xA, 6, 4, 6, 8, 13, 8, 0, 0xE, -1], # Atomic Fire
|
||||
2: [2, 0, 4, 0, 2, 0, 0, 0xA, 0, 0, 0, 0, 1, -1], # Air Shooter
|
||||
3: [0, 8, -1, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, -1], # Leaf Shield
|
||||
4: [6, 0, 0, -1, 0, 2, 0, 1, 0, 14, 1, 0, 0, 1], # Bubble Lead
|
||||
5: [2, 2, 0, 2, 0, 0, 4, 1, 1, 7, 2, 0, 1, -1], # Quick Boomerang
|
||||
6: [-1, 0, 2, 2, 4, 3, 0, 0, 1, 0, 1, 0x14, 1, -1], # Crash Bomber
|
||||
7: [1, 0, 2, 4, 0, 4, 0xE, 0, 0, 7, 0, 0, 1, -1], # Metal Blade
|
||||
8: [0, 0, 0, 0, 2, 0, 0, 0, 0, 0, 0, 0, 0, 0], # Time Stopper
|
||||
}
|
||||
|
||||
weapons_to_name: Dict[int, str] = {
|
||||
1: names.atomic_fire,
|
||||
2: names.air_shooter,
|
||||
3: names.leaf_shield,
|
||||
4: names.bubble_lead,
|
||||
5: names.quick_boomerang,
|
||||
6: names.crash_bomber,
|
||||
7: names.metal_blade,
|
||||
8: names.time_stopper
|
||||
}
|
||||
|
||||
minimum_weakness_requirement: Dict[int, int] = {
|
||||
0: 1, # Mega Buster is free
|
||||
1: 14, # 2 shots of Atomic Fire
|
||||
2: 1, # 14 shots of Air Shooter, although you likely hit more than one shot
|
||||
3: 4, # 9 uses of Leaf Shield, 3 ends up 1 damage off
|
||||
4: 1, # 56 uses of Bubble Lead
|
||||
5: 1, # 224 uses of Quick Boomerang
|
||||
6: 4, # 7 uses of Crash Bomber
|
||||
7: 1, # 112 uses of Metal Blade
|
||||
8: 4, # 1 use of Time Stopper, but setting to 4 means we shave the entire HP bar
|
||||
}
|
||||
|
||||
robot_masters: Dict[int, str] = {
|
||||
0: "Heat Man Defeated",
|
||||
1: "Air Man Defeated",
|
||||
2: "Wood Man Defeated",
|
||||
3: "Bubble Man Defeated",
|
||||
4: "Quick Man Defeated",
|
||||
5: "Flash Man Defeated",
|
||||
6: "Metal Man Defeated",
|
||||
7: "Crash Man Defeated"
|
||||
}
|
||||
|
||||
weapon_costs = {
|
||||
0: 0,
|
||||
1: 10,
|
||||
2: 2,
|
||||
3: 3,
|
||||
4: 0.5,
|
||||
5: 0.125,
|
||||
6: 4,
|
||||
7: 0.25,
|
||||
8: 7,
|
||||
}
|
||||
|
||||
|
||||
def can_defeat_enough_rbms(state: "CollectionState", player: int,
|
||||
required: int, boss_requirements: Dict[int, List[int]]):
|
||||
can_defeat = 0
|
||||
for boss, reqs in boss_requirements.items():
|
||||
if boss in robot_masters:
|
||||
if state.has_all(map(lambda x: weapons_to_name[x], reqs), player):
|
||||
can_defeat += 1
|
||||
if can_defeat >= required:
|
||||
return True
|
||||
return False
|
||||
|
||||
|
||||
def set_rules(world: "MM2World") -> None:
|
||||
# most rules are set on region, so we only worry about rules required within stage access
|
||||
# or rules variable on settings
|
||||
if (hasattr(world.multiworld, "re_gen_passthrough")
|
||||
and "Mega Man 2" in getattr(world.multiworld, "re_gen_passthrough")):
|
||||
slot_data = getattr(world.multiworld, "re_gen_passthrough")["Mega Man 2"]
|
||||
world.weapon_damage = slot_data["weapon_damage"]
|
||||
world.wily_5_weapons = slot_data["wily_5_weapons"]
|
||||
else:
|
||||
if world.options.random_weakness == RandomWeaknesses.option_shuffled:
|
||||
weapon_tables = [table for weapon, table in weapon_damage.items() if weapon not in (0, 8)]
|
||||
world.random.shuffle(weapon_tables)
|
||||
for i in range(1, 8):
|
||||
world.weapon_damage[i] = weapon_tables.pop()
|
||||
# alien must take minimum required damage from his weakness
|
||||
alien_weakness = next(weapon for weapon in range(8) if world.weapon_damage[weapon][13] != -1)
|
||||
world.weapon_damage[alien_weakness][13] = minimum_weakness_requirement[alien_weakness]
|
||||
world.weapon_damage[8] = [0 for _ in range(14)]
|
||||
world.weapon_damage[8][world.random.choice(range(8))] = 2
|
||||
elif world.options.random_weakness == RandomWeaknesses.option_randomized:
|
||||
world.weapon_damage = {i: [] for i in range(9)}
|
||||
for boss in range(13):
|
||||
for weapon in world.weapon_damage:
|
||||
world.weapon_damage[weapon].append(min(14, max(-1, int(world.random.normalvariate(3, 3)))))
|
||||
if not any([world.weapon_damage[weapon][boss] >= max(4, minimum_weakness_requirement[weapon])
|
||||
for weapon in range(1, 7)]):
|
||||
# failsafe, there should be at least one defined non-Buster weakness
|
||||
weapon = world.random.randint(1, 7)
|
||||
world.weapon_damage[weapon][boss] = world.random.randint(
|
||||
max(4, minimum_weakness_requirement[weapon]), 14) # Force weakness
|
||||
# special case, if boobeam trap has a weakness to Crash, it needs to be max damage
|
||||
if world.weapon_damage[6][11] > 4:
|
||||
world.weapon_damage[6][11] = 14
|
||||
# handle the alien
|
||||
boss = 13
|
||||
for weapon in world.weapon_damage:
|
||||
world.weapon_damage[weapon].append(-1)
|
||||
weapon = world.random.choice(list(world.weapon_damage.keys()))
|
||||
world.weapon_damage[weapon][boss] = minimum_weakness_requirement[weapon]
|
||||
|
||||
if world.options.strict_weakness:
|
||||
for weapon in weapon_damage:
|
||||
for i in range(13):
|
||||
if weapon == 0:
|
||||
world.weapon_damage[weapon][i] = 0
|
||||
elif i in (8, 12) and not world.options.random_weakness:
|
||||
continue
|
||||
# Mecha Dragon only has damage range of 0-1, so allow the 1
|
||||
# Wily Machine needs all three weaknesses present, so allow
|
||||
elif 4 > world.weapon_damage[weapon][i] > 0:
|
||||
world.weapon_damage[weapon][i] = 0
|
||||
# handle special cases
|
||||
for boss in range(14):
|
||||
for weapon in (1, 3, 6, 8):
|
||||
if (0 < world.weapon_damage[weapon][boss] < minimum_weakness_requirement[weapon] and
|
||||
not any(world.weapon_damage[i][boss] > 0 for i in range(1, 8) if i != weapon)):
|
||||
# Weapon does not have enough possible ammo to kill the boss, raise the damage
|
||||
if boss == 9:
|
||||
if weapon != 3:
|
||||
# Atomic Fire and Crash Bomber cannot be Picopico-kun's only weakness
|
||||
world.weapon_damage[weapon][boss] = 0
|
||||
weakness = world.random.choice((2, 3, 4, 5, 7, 8))
|
||||
world.weapon_damage[weakness][boss] = minimum_weakness_requirement[weakness]
|
||||
elif boss == 11:
|
||||
if weapon == 1:
|
||||
# Atomic Fire cannot be Boobeam Trap's only weakness
|
||||
world.weapon_damage[weapon][boss] = 0
|
||||
weakness = world.random.choice((2, 3, 4, 5, 6, 7, 8))
|
||||
world.weapon_damage[weakness][boss] = minimum_weakness_requirement[weakness]
|
||||
else:
|
||||
world.weapon_damage[weapon][boss] = minimum_weakness_requirement[weapon]
|
||||
starting = world.options.starting_robot_master.value
|
||||
world.weapon_damage[0][starting] = 1
|
||||
|
||||
for p_boss in world.options.plando_weakness:
|
||||
for p_weapon in world.options.plando_weakness[p_boss]:
|
||||
if world.options.plando_weakness[p_boss][p_weapon] < minimum_weakness_requirement[p_weapon] \
|
||||
and not any(w != p_weapon
|
||||
and world.weapon_damage[w][bosses[p_boss]] > minimum_weakness_requirement[w]
|
||||
for w in world.weapon_damage):
|
||||
# we need to replace this weakness
|
||||
weakness = world.random.choice([key for key in world.weapon_damage if key != p_weapon])
|
||||
world.weapon_damage[weakness][bosses[p_boss]] = minimum_weakness_requirement[weakness]
|
||||
world.weapon_damage[weapons_to_id[p_weapon]][bosses[p_boss]] \
|
||||
= world.options.plando_weakness[p_boss][p_weapon]
|
||||
|
||||
if world.weapon_damage[0][world.options.starting_robot_master.value] < 1:
|
||||
world.weapon_damage[0][world.options.starting_robot_master.value] = weapon_damage[0][world.options.starting_robot_master.value]
|
||||
|
||||
# final special case
|
||||
# There's a vanilla crash if Time Stopper kills Wily phase 1
|
||||
# There's multiple fixes, but ensuring Wily cannot take Time Stopper damage is best
|
||||
if world.weapon_damage[8][12] > 0:
|
||||
world.weapon_damage[8][12] = 0
|
||||
|
||||
# weakness validation, it is better to confirm a completable seed than respect plando
|
||||
boss_health = {boss: 0x1C if boss != 12 else 0x1C * 2 for boss in [*range(8), 12]}
|
||||
|
||||
weapon_energy = {key: float(0x1C) for key in weapon_costs}
|
||||
weapon_boss = {boss: {weapon: world.weapon_damage[weapon][boss] for weapon in world.weapon_damage}
|
||||
for boss in [*range(8), 12]}
|
||||
flexibility = {
|
||||
boss: (
|
||||
sum(damage_value > 0 for damage_value in
|
||||
weapon_damages.values()) # Amount of weapons that hit this boss
|
||||
* sum(weapon_damages.values()) # Overall damage that those weapons do
|
||||
)
|
||||
for boss, weapon_damages in weapon_boss.items() if boss != 12
|
||||
}
|
||||
flexibility = sorted(flexibility, key=flexibility.get) # Fast way to sort dict by value
|
||||
used_weapons = {i: set() for i in [*range(8), 12]}
|
||||
for boss in [*flexibility, 12]:
|
||||
boss_damage = weapon_boss[boss]
|
||||
weapon_weight = {weapon: (weapon_energy[weapon] / damage) if damage else 0 for weapon, damage in
|
||||
boss_damage.items() if weapon_energy[weapon] > 0}
|
||||
if boss_damage[8]:
|
||||
boss_damage[8] = 1.75 * boss_damage[8]
|
||||
if any(boss_damage[i] > 0 for i in range(8)) and 8 in weapon_weight:
|
||||
# We get exactly one use of Time Stopper during the rush
|
||||
# So we want to make sure that use is absolutely needed
|
||||
weapon_weight[8] = min(weapon_weight[8], 0.001)
|
||||
while boss_health[boss] > 0:
|
||||
if boss_damage[0] > 0:
|
||||
boss_health[boss] = 0 # if we can buster, we should buster
|
||||
continue
|
||||
highest, wp = max(zip(weapon_weight.values(), weapon_weight.keys()))
|
||||
uses = weapon_energy[wp] // weapon_costs[wp]
|
||||
used_weapons[boss].add(wp)
|
||||
if int(uses * boss_damage[wp]) > boss_health[boss]:
|
||||
used = ceil(boss_health[boss] / boss_damage[wp])
|
||||
weapon_energy[wp] -= weapon_costs[wp] * used
|
||||
boss_health[boss] = 0
|
||||
elif highest <= 0:
|
||||
# we are out of weapons that can actually damage the boss
|
||||
# so find the weapon that has the most uses, and apply that as an additional weakness
|
||||
# it should be impossible to be out of energy, simply because even if every boss took 1 from
|
||||
# Quick Boomerang and no other, it would only be 28 off from defeating all 9, which Metal Blade should
|
||||
# be able to cover
|
||||
wp, max_uses = max((weapon, weapon_energy[weapon] // weapon_costs[weapon]) for weapon in weapon_weight
|
||||
if weapon != 0)
|
||||
world.weapon_damage[wp][boss] = minimum_weakness_requirement[wp]
|
||||
used = min(int(weapon_energy[wp] // weapon_costs[wp]),
|
||||
ceil(boss_health[boss] // minimum_weakness_requirement[wp]))
|
||||
weapon_energy[wp] -= weapon_costs[wp] * used
|
||||
boss_health[boss] -= int(used * minimum_weakness_requirement[wp])
|
||||
weapon_weight.pop(wp)
|
||||
else:
|
||||
# drain the weapon and continue
|
||||
boss_health[boss] -= int(uses * boss_damage[wp])
|
||||
weapon_energy[wp] -= weapon_costs[wp] * uses
|
||||
weapon_weight.pop(wp)
|
||||
|
||||
world.wily_5_weapons = {boss: sorted(used_weapons[boss]) for boss in used_weapons}
|
||||
|
||||
for i, boss_locations in enumerate([
|
||||
heat_man_locations,
|
||||
air_man_locations,
|
||||
wood_man_locations,
|
||||
bubble_man_locations,
|
||||
quick_man_locations,
|
||||
flash_man_locations,
|
||||
metal_man_locations,
|
||||
crash_man_locations,
|
||||
wily_1_locations,
|
||||
wily_2_locations,
|
||||
wily_3_locations,
|
||||
wily_4_locations,
|
||||
wily_5_locations,
|
||||
wily_6_locations
|
||||
]):
|
||||
if world.weapon_damage[0][i] > 0:
|
||||
continue # this can always be in logic
|
||||
weapons = []
|
||||
for weapon in range(1, 9):
|
||||
if world.weapon_damage[weapon][i] > 0:
|
||||
if world.weapon_damage[weapon][i] < minimum_weakness_requirement[weapon]:
|
||||
continue # Atomic Fire can only be considered logical for bosses it can kill in 2 hits
|
||||
weapons.append(weapons_to_name[weapon])
|
||||
if not weapons:
|
||||
raise Exception(f"Attempted to have boss {i} with no weakness! Seed: {world.multiworld.seed}")
|
||||
for location in boss_locations:
|
||||
if i == 12:
|
||||
add_rule(world.get_location(location),
|
||||
lambda state, weps=tuple(weapons): state.has_all(weps, world.player))
|
||||
# TODO: when has_list gets added, check for a subset of possible weaknesses
|
||||
else:
|
||||
add_rule(world.get_location(location),
|
||||
lambda state, weps=tuple(weapons): state.has_any(weps, world.player))
|
||||
|
||||
# Always require Crash Bomber for Boobeam Trap
|
||||
add_rule(world.get_location(names.wily_4),
|
||||
lambda state: state.has(names.crash_bomber, world.player))
|
||||
add_rule(world.get_location(names.wily_stage_4),
|
||||
lambda state: state.has(names.crash_bomber, world.player))
|
||||
|
||||
# Need to defeat x amount of robot masters for Wily 5
|
||||
add_rule(world.get_location(names.wily_5),
|
||||
lambda state: can_defeat_enough_rbms(state, world.player, world.options.wily_5_requirement.value,
|
||||
world.wily_5_weapons))
|
||||
add_rule(world.get_location(names.wily_stage_5),
|
||||
lambda state: can_defeat_enough_rbms(state, world.player, world.options.wily_5_requirement.value,
|
||||
world.wily_5_weapons))
|
||||
|
||||
if not world.options.yoku_jumps:
|
||||
add_rule(world.get_entrance("To Heat Man Stage"),
|
||||
lambda state: state.has(names.item_2, world.player))
|
||||
|
||||
if not world.options.enable_lasers:
|
||||
add_rule(world.get_entrance("To Quick Man Stage"),
|
||||
lambda state: state.has(names.time_stopper, world.player))
|
||||
|
||||
if world.options.consumables in (Consumables.option_1up_etank,
|
||||
Consumables.option_all):
|
||||
add_rule(world.get_location(names.flash_man_c2),
|
||||
lambda state: state.has_any([names.item_1, names.item_2, names.item_3], world.player))
|
||||
add_rule(world.get_location(names.quick_man_c1),
|
||||
lambda state: state.has_any([names.item_1, names.item_2, names.item_3], world.player))
|
||||
add_rule(world.get_location(names.metal_man_c2),
|
||||
lambda state: state.has_any([names.item_1, names.item_2], world.player))
|
||||
add_rule(world.get_location(names.metal_man_c3),
|
||||
lambda state: state.has_any([names.item_1, names.item_2], world.player))
|
||||
add_rule(world.get_location(names.crash_man_c3),
|
||||
lambda state: state.has_any([names.item_1, names.item_2, names.item_3], world.player))
|
||||
add_rule(world.get_location(names.wily_2_c5),
|
||||
lambda state: state.has(names.crash_bomber, world.player))
|
||||
add_rule(world.get_location(names.wily_2_c6),
|
||||
lambda state: state.has(names.crash_bomber, world.player))
|
||||
add_rule(world.get_location(names.wily_3_c2),
|
||||
lambda state: state.has(names.crash_bomber, world.player))
|
||||
if world.options.consumables in (Consumables.option_weapon_health,
|
||||
Consumables.option_all):
|
||||
add_rule(world.get_location(names.flash_man_c3),
|
||||
lambda state: state.has(names.crash_bomber, world.player))
|
||||
add_rule(world.get_location(names.flash_man_c4),
|
||||
lambda state: state.has(names.crash_bomber, world.player))
|
||||
add_rule(world.get_location(names.wily_3_c1),
|
||||
lambda state: state.has(names.crash_bomber, world.player))
|
||||
861
worlds/mm2/src/mm2_basepatch.asm
Normal file
861
worlds/mm2/src/mm2_basepatch.asm
Normal file
@@ -0,0 +1,861 @@
|
||||
norom
|
||||
!headersize = 16
|
||||
|
||||
!controller_mirror = $23
|
||||
!controller_flip = $27 ; only on first frame of input, used by crash man, etc
|
||||
!current_stage = $2A
|
||||
!received_stages = $8A
|
||||
!completed_stages = $8B
|
||||
!received_item_checks = $8C
|
||||
!last_wily = $8D
|
||||
!deathlink = $8F
|
||||
!energylink_packet = $90
|
||||
!rbm_strobe = $91
|
||||
!received_weapons = $9A
|
||||
!received_items = $9B
|
||||
!current_weapon = $A9
|
||||
|
||||
!stage_completion = $0F70
|
||||
!consumable_checks = $0F80
|
||||
|
||||
!CONTROLLER_SELECT = #$04
|
||||
!CONTROLLER_SELECT_START = #$0C
|
||||
!CONTROLLER_ALL_BUTTON = #$0F
|
||||
|
||||
!PpuControl_2000 = $2000
|
||||
!PpuMask_2001 = $2001
|
||||
!PpuAddr_2006 = $2006
|
||||
!PpuData_2007 = $2007
|
||||
|
||||
!LOAD_BANK = $C000
|
||||
|
||||
macro org(address,bank)
|
||||
if <bank> == $0F
|
||||
org <address>-$C000+($4000*<bank>)+!headersize ; org sets the position in the output file to write to (in norom, at least)
|
||||
base <address> ; base sets the position that all labels are relative to - this is necessary so labels will still start from $8000, instead of $0000 or somewhere
|
||||
else
|
||||
org <address>-$8000+($4000*<bank>)+!headersize
|
||||
base <address>
|
||||
endif
|
||||
endmacro
|
||||
|
||||
%org($8400, $08)
|
||||
incbin "mm2font.dat"
|
||||
|
||||
%org($A900, $09)
|
||||
incbin "mm2titlefont.dat"
|
||||
|
||||
%org($807E, $0B)
|
||||
FlashFixes:
|
||||
CMP #$FF
|
||||
BEQ FlashFixTarget1
|
||||
CMP #$FF
|
||||
BNE FlashFixTarget2
|
||||
|
||||
%org($8086, $0B)
|
||||
FlashFixTarget1:
|
||||
|
||||
%org($808D, $0B)
|
||||
FlashFixTarget2:
|
||||
|
||||
%org($8015, $0D)
|
||||
ClearRefreshHook:
|
||||
; if we're already doing a fresh load of the stage select
|
||||
; we don't need to immediately refresh it
|
||||
JSR ClearRefresh
|
||||
NOP
|
||||
|
||||
%org($802B, $0D)
|
||||
PatchFaceTiles:
|
||||
LDA !received_stages
|
||||
|
||||
%org($8072, $0D)
|
||||
PatchFaceSprites:
|
||||
LDA !received_stages
|
||||
|
||||
%org($80CC, $0D)
|
||||
CheckItemsForWily:
|
||||
LDA !received_items
|
||||
CMP #$07
|
||||
|
||||
%org($80D2, $0D)
|
||||
LoadWily:
|
||||
JSR GoToMostRecentWily
|
||||
NOP
|
||||
|
||||
%org($80DC, $0D)
|
||||
CheckAccessCodes:
|
||||
LDA !received_stages
|
||||
|
||||
%org($8312, $0D)
|
||||
HookStageSelect:
|
||||
JSR RefreshRBMTiles
|
||||
NOP
|
||||
|
||||
%org($A315, $0D)
|
||||
RemoveWeaponClear:
|
||||
NOP
|
||||
NOP
|
||||
NOP
|
||||
NOP
|
||||
|
||||
;Adjust Password select flasher
|
||||
%org($A32A, $0D)
|
||||
LDX #$68
|
||||
|
||||
;Block password input
|
||||
%org($A346, $0D)
|
||||
EOR #$00
|
||||
|
||||
;Remove password text
|
||||
%org($AF3A, $0D)
|
||||
StartHeight:
|
||||
db $AC ; set Start to center
|
||||
|
||||
%org($AF49, $0D)
|
||||
PasswordText:
|
||||
db $40, $40, $40, $40, $40, $40, $40, $40
|
||||
|
||||
%org($AF6C, $0D)
|
||||
ContinueHeight:
|
||||
db $AB ; split height between 2 remaining options
|
||||
|
||||
%org($AF77, $0D)
|
||||
StageSelectHeight:
|
||||
db $EB ; split between 2 remaining options
|
||||
|
||||
%org($AF88, $0D)
|
||||
GameOverPasswordText:
|
||||
db $40, $40, $40, $40, $40, $40, $40, $40
|
||||
|
||||
%org($AFA5, $0D)
|
||||
GetEquippedPasswordText:
|
||||
db $40, $40, $40, $40, $40, $40, $40, $40
|
||||
|
||||
%org($AFAE, $0D)
|
||||
GetEquippedStageSelect:
|
||||
db $26, $EA
|
||||
|
||||
%org($B195, $0D)
|
||||
GameOverPasswordUp:
|
||||
LDA #$01 ; originally 02, removing last option
|
||||
|
||||
%org($B19F, $0D)
|
||||
GameOverPassword:
|
||||
CMP #$02 ; originally 03, remove the last option
|
||||
|
||||
%org($B1ED, $0D)
|
||||
FixupGameOverArrows:
|
||||
db $68, $78
|
||||
|
||||
%org($BB74, $0D)
|
||||
GetEquippedStage:
|
||||
JSR StageGetEquipped
|
||||
NOP #13
|
||||
|
||||
%org($BBD9, $0D)
|
||||
GetEquippedDefault:
|
||||
LDA #$01
|
||||
|
||||
%org($BC01, $0D)
|
||||
GetEquippedPasswordRemove:
|
||||
ORA #$01 ; originally EOR #$01, we always want 1 here
|
||||
|
||||
%org($BCF1, $0D)
|
||||
GetEquippedItem:
|
||||
ADC #$07
|
||||
JSR ItemGetEquipped
|
||||
JSR LoadItemsColor
|
||||
NOP ; !!!! This is a load-bearing NOP. It gets branched to later in the function
|
||||
LDX $FF
|
||||
|
||||
|
||||
%org($BB08, $0D)
|
||||
WilyProgress:
|
||||
JSR StoreWilyProgress
|
||||
NOP
|
||||
|
||||
%org($BF6F, $0D)
|
||||
GetEquippedStageSelectHeight:
|
||||
db $B8
|
||||
|
||||
%org($805B, $0E)
|
||||
InitalizeStartingRBM:
|
||||
LDA #$FF ; this does two things
|
||||
STA !received_stages ; we're overwriting clearing e-tanks and setting RBM available to none
|
||||
|
||||
%org($8066, $0E)
|
||||
BlockStartupAutoWily:
|
||||
; presumably this would be called from password?
|
||||
LDA #$00
|
||||
|
||||
%org($80A7, $0E)
|
||||
StageLoad:
|
||||
JMP CleanWily5
|
||||
NOP
|
||||
|
||||
%org($8178, $0E)
|
||||
Main1:
|
||||
JSR MainLoopHook
|
||||
NOP
|
||||
|
||||
%org($81DE, $0E)
|
||||
Wily5Teleporter:
|
||||
LDA $99
|
||||
CMP #$01
|
||||
BCC SkipSpawn
|
||||
|
||||
%org($81F9, $0E)
|
||||
SkipSpawn:
|
||||
; just present to fix the branch, if we try to branch raw it'll get confused
|
||||
|
||||
%org($822D, $0E)
|
||||
Main2:
|
||||
; believe used in the wily 5 refights?
|
||||
JSR MainLoopHook
|
||||
NOP
|
||||
|
||||
%org($842F, $0E)
|
||||
Wily5Hook:
|
||||
JMP Wily5Requirement
|
||||
NOP
|
||||
|
||||
%org($C10D, $0F)
|
||||
Deathlink:
|
||||
JSR KillMegaMan
|
||||
|
||||
%org($C1BC, $0F)
|
||||
RemoveETankLoss:
|
||||
NOP
|
||||
NOP
|
||||
|
||||
%org($C23C, $0F)
|
||||
WriteStageComplete:
|
||||
ORA !completed_stages
|
||||
STA !completed_stages
|
||||
|
||||
%org($C243, $0F)
|
||||
WriteReceiveItem:
|
||||
ORA !received_item_checks
|
||||
STA !received_item_checks
|
||||
|
||||
%org($C254, $0F)
|
||||
BlockAutoWily:
|
||||
; and this one is on return from stage?
|
||||
LDA #$00
|
||||
|
||||
%org($C261, $0F)
|
||||
WilyStageCompletion:
|
||||
JSR StoreWilyStageCompletion
|
||||
NOP
|
||||
|
||||
%org($E5AC, $0F)
|
||||
NullDeathlink:
|
||||
STA $8F ; we null his HP later in the process
|
||||
NOP
|
||||
|
||||
%org($E5D1, $0F)
|
||||
EnergylinkHook:
|
||||
JSR Energylink
|
||||
NOP #2 ; comment this out to enable item giving their usual reward alongside EL
|
||||
|
||||
%org($E5E8, $0F)
|
||||
ConsumableHook:
|
||||
JSR CheckConsumable
|
||||
|
||||
%org($F2E3, $0F)
|
||||
|
||||
CheckConsumable:
|
||||
STA $0140, Y
|
||||
TXA
|
||||
PHA
|
||||
LDA $AD ; the consumable value
|
||||
CMP #$7C
|
||||
BPL .Store
|
||||
print "Consumables (replace 7a): ", hex(realbase())
|
||||
CMP #$76
|
||||
BMI .Store
|
||||
LDA #$00
|
||||
.Store:
|
||||
STA $AD
|
||||
LDA $2A
|
||||
ASL
|
||||
ASL
|
||||
TAX
|
||||
TYA
|
||||
.LoopHead:
|
||||
CMP #$08
|
||||
BMI .GetFlag
|
||||
INX
|
||||
SBC #$08
|
||||
BNE .LoopHead
|
||||
.GetFlag:
|
||||
TAY
|
||||
LDA #$01
|
||||
.Loop2Head:
|
||||
CPY #$00
|
||||
BEQ .Apply
|
||||
ASL
|
||||
DEY
|
||||
BNE .Loop2Head
|
||||
.Apply:
|
||||
ORA !consumable_checks, X
|
||||
STA !consumable_checks, X
|
||||
PLA
|
||||
TAX
|
||||
RTS
|
||||
|
||||
GoToMostRecentWily:
|
||||
LDA !controller_mirror
|
||||
CMP !CONTROLLER_SELECT_START
|
||||
BEQ .Default
|
||||
LDA !last_wily
|
||||
BNE .Store
|
||||
.Default:
|
||||
LDA #$08 ; wily stage 1
|
||||
.Store:
|
||||
STA !current_stage
|
||||
RTS
|
||||
|
||||
StoreWilyStageCompletion:
|
||||
LDA #$01
|
||||
STA !stage_completion, X
|
||||
INC !current_stage
|
||||
LDA !current_stage
|
||||
STA !last_wily
|
||||
RTS
|
||||
|
||||
ReturnToGameOver:
|
||||
LDA #$10
|
||||
STA !PpuControl_2000
|
||||
LDA #$06
|
||||
STA !PpuMask_2001
|
||||
JMP $C1BE ; specific code that loads game over
|
||||
|
||||
MainLoopHook:
|
||||
LDA !controller_mirror
|
||||
CMP !CONTROLLER_ALL_BUTTON
|
||||
BNE .Next
|
||||
JMP ReturnToGameOver
|
||||
.Next:
|
||||
LDA !deathlink
|
||||
CMP #$01
|
||||
BNE .Next2
|
||||
JMP $E5A8 ; this kills the Mega Man
|
||||
.Next2:
|
||||
print "Quickswap:", hex(realbase())
|
||||
LDA #$00 ; slot data, write in enable for quickswap
|
||||
CMP #$01
|
||||
BNE .Finally
|
||||
LDA !controller_flip
|
||||
AND !CONTROLLER_SELECT
|
||||
BEQ .Finally
|
||||
JMP Quickswap
|
||||
.Finally:
|
||||
LDA !controller_flip
|
||||
AND #$08 ; this is checking for menu
|
||||
RTS
|
||||
|
||||
StoreWilyProgress:
|
||||
STA !current_stage
|
||||
TXA
|
||||
PHA
|
||||
LDX !current_stage
|
||||
LDA #$01
|
||||
STA !stage_completion, X
|
||||
PLA
|
||||
TAX
|
||||
print "Get Equipped Music: ", hex(realbase())
|
||||
LDA #$17
|
||||
RTS
|
||||
|
||||
KillMegaMan:
|
||||
JSR $C051 ; this kills the mega man
|
||||
LDA #$00
|
||||
STA $06C0 ; set HP to zero so client can actually detect he died
|
||||
RTS
|
||||
|
||||
Wily5Requirement:
|
||||
LDA #$01
|
||||
LDX #$08
|
||||
LDY #$00
|
||||
.LoopHead:
|
||||
BIT $BC
|
||||
BEQ .Skip
|
||||
INY
|
||||
.Skip:
|
||||
DEX
|
||||
ASL
|
||||
CPX #$00
|
||||
BNE .LoopHead
|
||||
print "Wily 5 Requirement:", hex(realbase())
|
||||
CPY #$08
|
||||
BCS .SpawnTeleporter
|
||||
JMP $8450
|
||||
.SpawnTeleporter:
|
||||
LDA #$FF
|
||||
STA $BC
|
||||
LDA #$01
|
||||
STA $99
|
||||
JMP $8433
|
||||
|
||||
CleanWily5:
|
||||
LDA #$00
|
||||
STA $BC
|
||||
STA $99
|
||||
JMP $80AB
|
||||
|
||||
LoadString:
|
||||
STY $00
|
||||
ASL
|
||||
ASL
|
||||
ASL
|
||||
ASL
|
||||
TAY
|
||||
LDA $DB
|
||||
ADC #$00
|
||||
STA $C8
|
||||
LDA #$40
|
||||
STA $C9
|
||||
LDA #$F6
|
||||
CLC
|
||||
ADC $C8
|
||||
STA $CA
|
||||
LDA ($C9), Y
|
||||
STA $03B6
|
||||
TYA
|
||||
CLC
|
||||
ADC #$01
|
||||
TAY
|
||||
LDA $CA
|
||||
ADC #$00
|
||||
STA $CA
|
||||
LDA ($C9), Y
|
||||
STA $03B7
|
||||
TYA
|
||||
CLC
|
||||
ADC #$01
|
||||
TAY
|
||||
LDA $CA
|
||||
ADC #$00
|
||||
STA $CA
|
||||
STY $FE
|
||||
LDA #$0E
|
||||
STA $FD
|
||||
.LoopHead:
|
||||
JSR $BD34
|
||||
LDY $FE
|
||||
CPY #$40
|
||||
BNE .NotEqual
|
||||
LDA $0420
|
||||
BNE .Skip
|
||||
.NotEqual:
|
||||
LDA ($C9), Y
|
||||
.Skip:
|
||||
STA $03B8
|
||||
INC $47
|
||||
INC $03B7
|
||||
LDA $FE
|
||||
CLC
|
||||
ADC #$01
|
||||
STA $FE
|
||||
LDA $CA
|
||||
ADC #$00
|
||||
STA $CA
|
||||
DEC $FD
|
||||
BNE .LoopHead
|
||||
LDY $00
|
||||
JSR $C0AB
|
||||
RTS
|
||||
|
||||
StageGetEquipped:
|
||||
LDA !current_stage
|
||||
LDX #$00
|
||||
BCS LoadGetEquipped
|
||||
ItemGetEquipped:
|
||||
LDX #$02
|
||||
LoadGetEquipped:
|
||||
STX $DB
|
||||
ASL
|
||||
ASL
|
||||
PHA
|
||||
SEC
|
||||
JSR LoadString
|
||||
PLA
|
||||
ADC #$00
|
||||
PHA
|
||||
SEC
|
||||
JSR LoadString
|
||||
PLA
|
||||
ADC #$00
|
||||
PHA
|
||||
SEC
|
||||
JSR LoadString
|
||||
LDA #$00
|
||||
SEC
|
||||
JSR $BD3E
|
||||
PLA
|
||||
ADC #$00
|
||||
SEC
|
||||
JSR LoadString
|
||||
RTS
|
||||
|
||||
LoadItemsColor:
|
||||
LDA #$7D
|
||||
STA $FD
|
||||
LDA $0420
|
||||
AND #$0F
|
||||
ASL
|
||||
SEC
|
||||
ADC #$1A
|
||||
STA $FF
|
||||
RTS
|
||||
|
||||
Energylink:
|
||||
LSR $0420, X
|
||||
print "Energylink: ", hex(realbase())
|
||||
LDA #$00
|
||||
BEQ .ApplyDrop
|
||||
LDA $04E0, X
|
||||
BEQ .ApplyDrop ; This is a stage pickup, and not an enemy drop
|
||||
STY !energylink_packet
|
||||
SEC
|
||||
BCS .Return
|
||||
.ApplyDrop:
|
||||
STY $AD
|
||||
.Return:
|
||||
RTS
|
||||
|
||||
|
||||
Quickswap:
|
||||
LDX #$0F
|
||||
.LoopHead:
|
||||
LDA $0420, X
|
||||
BMI .Return1 ; return if we have any weapon entities spawned
|
||||
DEX
|
||||
CPX #$01
|
||||
BNE .LoopHead
|
||||
LDX !current_weapon
|
||||
BNE .DoQuickswap
|
||||
LDX #$00
|
||||
.DoQuickswap:
|
||||
TYA
|
||||
PHA
|
||||
LDX !current_weapon
|
||||
INX
|
||||
CPX #$09
|
||||
BPL .Items
|
||||
LDA #$01
|
||||
.Loop2Head:
|
||||
DEX
|
||||
BEQ .FoundTarget
|
||||
ASL
|
||||
CPX #$00
|
||||
BNE .Loop2Head
|
||||
.FoundTarget:
|
||||
LDX !current_weapon
|
||||
INX
|
||||
.Loop3Head:
|
||||
PHA
|
||||
AND !received_weapons
|
||||
BNE .CanSwap
|
||||
PLA
|
||||
INX
|
||||
CPX #$09
|
||||
BPL .Items
|
||||
ASL
|
||||
BNE .Loop3Head
|
||||
.CanSwap:
|
||||
PLA
|
||||
SEC
|
||||
BCS .ApplySwap
|
||||
.Items:
|
||||
TXA
|
||||
PHA
|
||||
SEC
|
||||
SBC #$08
|
||||
TAX
|
||||
LDA #$01
|
||||
.Loop4Head:
|
||||
DEX
|
||||
BEQ .CheckItem
|
||||
ASL
|
||||
CPX #$00
|
||||
BNE .Loop4Head
|
||||
.CheckItem:
|
||||
TAY
|
||||
PLA
|
||||
TAX
|
||||
TYA
|
||||
.Loop5Head:
|
||||
PHA
|
||||
AND !received_items
|
||||
BNE .CanSwap
|
||||
PLA
|
||||
INX
|
||||
ASL
|
||||
BNE .Loop5Head
|
||||
LDX #$00
|
||||
SEC
|
||||
BCS .ApplySwap
|
||||
.Return1:
|
||||
RTS
|
||||
.ApplySwap: ; $F408 on old rom
|
||||
LDA #$0D
|
||||
JSR !LOAD_BANK
|
||||
; this is a bunch of boiler plate to make the swap work
|
||||
LDA $B5
|
||||
PHA
|
||||
LDA $B6
|
||||
PHA
|
||||
LDA $B7
|
||||
PHA
|
||||
LDA $B8
|
||||
PHA
|
||||
LDA $B9
|
||||
PHA
|
||||
LDA $20
|
||||
PHA
|
||||
LDA $1F
|
||||
PHA
|
||||
;but wait, there's more
|
||||
STX !current_weapon
|
||||
JSR $CC6C
|
||||
LDA $1A
|
||||
PHA
|
||||
LDX #$00
|
||||
.Loop6Head:
|
||||
STX $FD
|
||||
CLC
|
||||
LDA $52
|
||||
ADC $957F, X
|
||||
STA $08
|
||||
LDA $53
|
||||
ADC #$00
|
||||
STA $09
|
||||
LDA $08
|
||||
LSR $09
|
||||
ROR
|
||||
LSR $09
|
||||
ROR
|
||||
STA $08
|
||||
AND #$3F
|
||||
STA $1A
|
||||
CLC
|
||||
LDA $09
|
||||
ADC #$85
|
||||
STA $09
|
||||
LDA #$00
|
||||
STA $1B
|
||||
LDA $FD
|
||||
CMP #$08
|
||||
BCS .Past8
|
||||
LDX $A9
|
||||
LDA $9664, X
|
||||
TAY
|
||||
CPX #$09
|
||||
BCC .LessThanNine
|
||||
LDX #$00
|
||||
BEQ .Apply
|
||||
.LessThanNine:
|
||||
LDX #$05
|
||||
BNE .Apply
|
||||
.Past8:
|
||||
LDY #$90
|
||||
LDX #$00
|
||||
.Apply:
|
||||
JSR $C760
|
||||
JSR $C0AB ; iirc this is loading graphics?
|
||||
LDX $FD
|
||||
INX
|
||||
CPX #$0F
|
||||
BNE .Loop6Head
|
||||
STX $FD
|
||||
LDY #$90
|
||||
LDX #$00
|
||||
JSR $C760
|
||||
JSR $D2ED
|
||||
; two sections redacted here, might need to look at what they actually do?
|
||||
PLA
|
||||
STA $1A
|
||||
PLA
|
||||
STA $1F
|
||||
PLA
|
||||
STA $20
|
||||
PLA
|
||||
STA $B9
|
||||
PLA
|
||||
STA $B8
|
||||
PLA
|
||||
STA $B7
|
||||
PLA
|
||||
STA $B6
|
||||
PLA
|
||||
STA $B5
|
||||
LDA #$00
|
||||
STA $AC
|
||||
STA $2C
|
||||
STA $0680
|
||||
STA $06A0
|
||||
LDA #$1A
|
||||
STA $0400
|
||||
LDA #$03
|
||||
STA $AA
|
||||
LDA #$30
|
||||
JSR $C051
|
||||
.Finally:
|
||||
LDA #$0E
|
||||
JSR !LOAD_BANK
|
||||
PLA
|
||||
TAY
|
||||
.Return:
|
||||
RTS
|
||||
|
||||
RefreshRBMTiles:
|
||||
; primarily just a copy of the startup RBM setup, we just do it again
|
||||
; can't jump to it as it leads into the main loop
|
||||
LDA !rbm_strobe
|
||||
BNE .Update
|
||||
JMP .NoUpdate
|
||||
.Update:
|
||||
LDA #$00
|
||||
STA !rbm_strobe
|
||||
LDA #$10
|
||||
STA $F7
|
||||
STA !PpuControl_2000
|
||||
LDA #$06
|
||||
STA $F8
|
||||
STA !PpuMask_2001
|
||||
JSR $847E
|
||||
JSR $843C
|
||||
LDX #$00
|
||||
LDA $8A
|
||||
STA $01
|
||||
.TileLoop:
|
||||
STX $00
|
||||
LSR $01
|
||||
BCC .SkipTile
|
||||
LDA $8531,X
|
||||
STA $09
|
||||
LDA $8539,X
|
||||
STA $08
|
||||
LDX #$04
|
||||
LDA #$00
|
||||
.ClearBody:
|
||||
LDA $09
|
||||
STA !PpuAddr_2006
|
||||
LDA $08
|
||||
STA !PpuAddr_2006
|
||||
LDY #$04
|
||||
LDA #$00
|
||||
.ClearLine:
|
||||
STA !PpuData_2007
|
||||
DEY
|
||||
BNE .ClearLine
|
||||
CLC
|
||||
LDA $08
|
||||
ADC #$20
|
||||
STA $08
|
||||
DEX
|
||||
BNE .ClearBody
|
||||
.SkipTile:
|
||||
LDX $00
|
||||
INX
|
||||
CPX #$08
|
||||
BNE .TileLoop
|
||||
LDX #$1F
|
||||
JSR $829E
|
||||
JSR $8473
|
||||
LDX #$00
|
||||
LDA $8A
|
||||
STA $02
|
||||
LDY #$00
|
||||
.SpriteLoop:
|
||||
STX $01
|
||||
LSR $02
|
||||
BCS .SkipRBM
|
||||
LDA $8605,X
|
||||
STA $00
|
||||
LDA $85FD,X
|
||||
TAX
|
||||
.WriteSprite:
|
||||
LDA $8541,X
|
||||
STA $0200,Y
|
||||
INY
|
||||
INX
|
||||
DEC $00
|
||||
BNE .WriteSprite
|
||||
.SkipRBM:
|
||||
LDX $01
|
||||
INX
|
||||
CPX #$08
|
||||
BNE .SpriteLoop
|
||||
JSR $A51D
|
||||
LDA #$0C
|
||||
JSR $C051
|
||||
LDA #$00
|
||||
STA $2A
|
||||
STA $FD
|
||||
JSR $C0AB
|
||||
.NoUpdate:
|
||||
LDA $1C
|
||||
AND #$08
|
||||
RTS
|
||||
|
||||
ClearRefresh:
|
||||
LDA #$00
|
||||
STA !rbm_strobe
|
||||
LDA #$10
|
||||
STA $F7
|
||||
RTS
|
||||
|
||||
assert realbase() <= $03F650 ; This is the start of our text data, and we absolutely cannot go past this point (text takes too much room).
|
||||
|
||||
%org($F640, $0F)
|
||||
db $25, $4B, "PLACEHOLDER_L1"
|
||||
db $25, $6B, "PLACEHOLDER_L2"
|
||||
db $25, $8B, "PLACEHOLDER_L3"
|
||||
db $25, $EB, "PLACEHOLDER_PL"
|
||||
db $25, $4B, "PLACEHOLDER_L1"
|
||||
db $25, $6B, "PLACEHOLDER_L2"
|
||||
db $25, $8B, "PLACEHOLDER_L3"
|
||||
db $25, $EB, "PLACEHOLDER_PL"
|
||||
db $25, $4B, "PLACEHOLDER_L1"
|
||||
db $25, $6B, "PLACEHOLDER_L2"
|
||||
db $25, $8B, "PLACEHOLDER_L3"
|
||||
db $25, $EB, "PLACEHOLDER_PL"
|
||||
db $25, $4B, "PLACEHOLDER_L1"
|
||||
db $25, $6B, "PLACEHOLDER_L2"
|
||||
db $25, $8B, "PLACEHOLDER_L3"
|
||||
db $25, $EB, "PLACEHOLDER_PL"
|
||||
db $25, $4B, "PLACEHOLDER_L1"
|
||||
db $25, $6B, "PLACEHOLDER_L2"
|
||||
db $25, $8B, "PLACEHOLDER_L3"
|
||||
db $25, $EB, "PLACEHOLDER_PL"
|
||||
db $25, $4B, "PLACEHOLDER_L1"
|
||||
db $25, $6B, "PLACEHOLDER_L2"
|
||||
db $25, $8B, "PLACEHOLDER_L3"
|
||||
db $25, $EB, "PLACEHOLDER_PL"
|
||||
db $25, $4B, "PLACEHOLDER_L1"
|
||||
db $25, $6B, "PLACEHOLDER_L2"
|
||||
db $25, $8B, "PLACEHOLDER_L3"
|
||||
db $25, $EB, "PLACEHOLDER_PL"
|
||||
db $25, $4B, "PLACEHOLDER_L1"
|
||||
db $25, $6B, "PLACEHOLDER_L2"
|
||||
db $25, $8B, "PLACEHOLDER_L3"
|
||||
db $25, $EB, "PLACEHOLDER_PL"
|
||||
db $25, $4B, "PLACEHOLDER_L1"
|
||||
db $25, $6B, "PLACEHOLDER_L2"
|
||||
db $25, $8B, "PLACEHOLDER_L3"
|
||||
db $25, $EB, "PLACEHOLDER_PL"
|
||||
db $25, $4B, "PLACEHOLDER_L1"
|
||||
db $25, $6B, "PLACEHOLDER_L2"
|
||||
db $25, $8B, "PLACEHOLDER_L3"
|
||||
db $25, $EB, "PLACEHOLDER_PL"
|
||||
db $25, $4B, "PLACEHOLDER_L1"
|
||||
db $25, $6B, "PLACEHOLDER_L2"
|
||||
db $25, $8B, "PLACEHOLDER_L3"
|
||||
db $25, $EB, "PLACEHOLDER_PL"
|
||||
|
||||
%org($FFB0, $0F)
|
||||
db "MM2_BASEPATCH_ARCHI "
|
||||
BIN
worlds/mm2/src/mm2font.dat
Normal file
BIN
worlds/mm2/src/mm2font.dat
Normal file
Binary file not shown.
BIN
worlds/mm2/src/mm2titlefont.dat
Normal file
BIN
worlds/mm2/src/mm2titlefont.dat
Normal file
Binary file not shown.
5
worlds/mm2/test/__init__.py
Normal file
5
worlds/mm2/test/__init__.py
Normal file
@@ -0,0 +1,5 @@
|
||||
from test.bases import WorldTestBase
|
||||
|
||||
|
||||
class MM2TestBase(WorldTestBase):
|
||||
game = "Mega Man 2"
|
||||
47
worlds/mm2/test/test_access.py
Normal file
47
worlds/mm2/test/test_access.py
Normal file
@@ -0,0 +1,47 @@
|
||||
from . import MM2TestBase
|
||||
from ..locations import (quick_man_locations, heat_man_locations, wily_1_locations, wily_2_locations,
|
||||
wily_3_locations, wily_4_locations, wily_5_locations, wily_6_locations,
|
||||
energy_pickups, etank_1ups)
|
||||
from ..names import *
|
||||
|
||||
|
||||
class TestAccess(MM2TestBase):
|
||||
options = {
|
||||
"consumables": "all"
|
||||
}
|
||||
|
||||
def test_time_stopper(self) -> None:
|
||||
"""Optional based on Enable Lasers setting, confirm these are the locations affected"""
|
||||
locations = [*quick_man_locations, *energy_pickups["Quick Man Stage"], *etank_1ups["Quick Man Stage"]]
|
||||
items = [["Time Stopper"]]
|
||||
self.assertAccessDependency(locations, items)
|
||||
|
||||
def test_item_2(self) -> None:
|
||||
"""Optional based on Yoku Block setting, confirm these are the locations affected"""
|
||||
locations = [*heat_man_locations, *etank_1ups["Heat Man Stage"]]
|
||||
items = [["Item 2 - Rocket"]]
|
||||
self.assertAccessDependency(locations, items, True)
|
||||
|
||||
def test_any_item(self) -> None:
|
||||
locations = [flash_man_c2, quick_man_c1, crash_man_c3]
|
||||
items = [["Item 1 - Propeller"], ["Item 2 - Rocket"], ["Item 3 - Bouncy"]]
|
||||
self.assertAccessDependency(locations, items, True)
|
||||
locations = [metal_man_c2, metal_man_c3]
|
||||
items = [["Item 1 - Propeller"], ["Item 2 - Rocket"]]
|
||||
self.assertAccessDependency(locations, items, True)
|
||||
|
||||
def test_all_items(self) -> None:
|
||||
locations = [flash_man_c2, quick_man_c1, crash_man_c3, metal_man_c2, metal_man_c3, *heat_man_locations,
|
||||
*etank_1ups["Heat Man Stage"], *wily_1_locations, *wily_2_locations, *wily_3_locations,
|
||||
*wily_4_locations, *wily_5_locations, *wily_6_locations, *etank_1ups["Wily Stage 1"],
|
||||
*etank_1ups["Wily Stage 2"], *etank_1ups["Wily Stage 3"], *etank_1ups["Wily Stage 4"],
|
||||
*energy_pickups["Wily Stage 1"], *energy_pickups["Wily Stage 2"], *energy_pickups["Wily Stage 3"],
|
||||
*energy_pickups["Wily Stage 4"]]
|
||||
items = [["Item 1 - Propeller", "Item 2 - Rocket", "Item 3 - Bouncy"]]
|
||||
self.assertAccessDependency(locations, items)
|
||||
|
||||
def test_crash_bomber(self) -> None:
|
||||
locations = [flash_man_c3, flash_man_c4, wily_2_c5, wily_2_c6, wily_3_c1, wily_3_c2,
|
||||
wily_4, wily_stage_4]
|
||||
items = [["Crash Bomber"]]
|
||||
self.assertAccessDependency(locations, items)
|
||||
104
worlds/mm2/test/test_weakness.py
Normal file
104
worlds/mm2/test/test_weakness.py
Normal file
@@ -0,0 +1,104 @@
|
||||
from math import ceil
|
||||
|
||||
from . import MM2TestBase
|
||||
from ..options import bosses
|
||||
|
||||
|
||||
# Need to figure out how this test should work
|
||||
def validate_wily_5(base: MM2TestBase) -> None:
|
||||
world = base.multiworld.worlds[base.player]
|
||||
weapon_damage = world.weapon_damage
|
||||
weapon_costs = {
|
||||
0: 0,
|
||||
1: 10,
|
||||
2: 2,
|
||||
3: 3,
|
||||
4: 0.5,
|
||||
5: 0.125,
|
||||
6: 4,
|
||||
7: 0.25,
|
||||
8: 7,
|
||||
}
|
||||
boss_health = {boss: 0x1C if boss != 12 else 0x1C * 2 for boss in [*range(8), 12]}
|
||||
weapon_energy = {key: float(0x1C) for key in weapon_costs}
|
||||
weapon_boss = {boss: {weapon: world.weapon_damage[weapon][boss] for weapon in world.weapon_damage}
|
||||
for boss in [*range(8), 12]}
|
||||
flexibility = {
|
||||
boss: (
|
||||
sum(damage_value > 0 for damage_value in
|
||||
weapon_damages.values()) # Amount of weapons that hit this boss
|
||||
* sum(weapon_damages.values()) # Overall damage that those weapons do
|
||||
)
|
||||
for boss, weapon_damages in weapon_boss.items() if boss != 12
|
||||
}
|
||||
flexibility = sorted(flexibility, key=flexibility.get) # Fast way to sort dict by value
|
||||
used_weapons = {i: set() for i in [*range(8), 12]}
|
||||
for boss in [*flexibility, 12]:
|
||||
boss_damage = weapon_boss[boss]
|
||||
weapon_weight = {weapon: (weapon_energy[weapon] / damage) if damage else 0 for weapon, damage in
|
||||
boss_damage.items() if weapon_energy[weapon] > 0}
|
||||
if boss_damage[8]:
|
||||
boss_damage[8] = 1.75 * boss_damage[8]
|
||||
if any(boss_damage[i] > 0 for i in range(8)) and 8 in weapon_weight:
|
||||
# We get exactly one use of Time Stopper during the rush
|
||||
# So we want to make sure that use is absolutely needed
|
||||
weapon_weight[8] = min(weapon_weight[8], 0.001)
|
||||
while boss_health[boss] > 0:
|
||||
if boss_damage[0] > 0:
|
||||
boss_health[boss] = 0 # if we can buster, we should buster
|
||||
continue
|
||||
highest, wp = max(zip(weapon_weight.values(), weapon_weight.keys()))
|
||||
uses = weapon_energy[wp] // weapon_costs[wp]
|
||||
used_weapons[boss].add(wp)
|
||||
if int(uses * boss_damage[wp]) > boss_health[boss]:
|
||||
used = ceil(boss_health[boss] / boss_damage[wp])
|
||||
weapon_energy[wp] -= weapon_costs[wp] * used
|
||||
boss_health[boss] = 0
|
||||
elif highest <= 0:
|
||||
# we are out of weapons that can actually damage the boss
|
||||
base.fail(f"Ran out of weapon energy to damage "
|
||||
f"{next(name for name in bosses if bosses[name] == boss)}\n"
|
||||
f"Seed: {base.multiworld.seed}\n"
|
||||
f"Damage Table: {weapon_damage}")
|
||||
else:
|
||||
# drain the weapon and continue
|
||||
boss_health[boss] -= int(uses * boss_damage[wp])
|
||||
weapon_energy[wp] -= weapon_costs[wp] * uses
|
||||
weapon_weight.pop(wp)
|
||||
|
||||
|
||||
class StrictWeaknessTests(MM2TestBase):
|
||||
options = {
|
||||
"strict_weakness": True,
|
||||
"yoku_jumps": True,
|
||||
"enable_lasers": True
|
||||
}
|
||||
|
||||
def test_that_every_boss_has_a_weakness(self) -> None:
|
||||
world = self.multiworld.worlds[self.player]
|
||||
weapon_damage = world.weapon_damage
|
||||
for boss in range(14):
|
||||
if not any(weapon_damage[weapon][boss] for weapon in range(9)):
|
||||
self.fail(f"Boss {boss} generated without weakness! Seed: {self.multiworld.seed}")
|
||||
|
||||
def test_wily_5(self) -> None:
|
||||
validate_wily_5(self)
|
||||
|
||||
|
||||
class RandomStrictWeaknessTests(MM2TestBase):
|
||||
options = {
|
||||
"strict_weakness": True,
|
||||
"random_weakness": "randomized",
|
||||
"yoku_jumps": True,
|
||||
"enable_lasers": True
|
||||
}
|
||||
|
||||
def test_that_every_boss_has_a_weakness(self) -> None:
|
||||
world = self.multiworld.worlds[self.player]
|
||||
weapon_damage = world.weapon_damage
|
||||
for boss in range(14):
|
||||
if not any(weapon_damage[weapon][boss] for weapon in range(9)):
|
||||
self.fail(f"Boss {boss} generated without weakness! Seed: {self.multiworld.seed}")
|
||||
|
||||
def test_wily_5(self) -> None:
|
||||
validate_wily_5(self)
|
||||
90
worlds/mm2/text.py
Normal file
90
worlds/mm2/text.py
Normal file
@@ -0,0 +1,90 @@
|
||||
from typing import DefaultDict
|
||||
from collections import defaultdict
|
||||
|
||||
MM2_WEAPON_ENCODING: DefaultDict[str, int] = defaultdict(lambda x: 0x6F, {
|
||||
' ': 0x40,
|
||||
'A': 0x41,
|
||||
'B': 0x42,
|
||||
'C': 0x43,
|
||||
'D': 0x44,
|
||||
'E': 0x45,
|
||||
'F': 0x46,
|
||||
'G': 0x47,
|
||||
'H': 0x48,
|
||||
'I': 0x49,
|
||||
'J': 0x4A,
|
||||
'K': 0x4B,
|
||||
'L': 0x4C,
|
||||
'M': 0x4D,
|
||||
'N': 0x4E,
|
||||
'O': 0x4F,
|
||||
'P': 0x50,
|
||||
'Q': 0x51,
|
||||
'R': 0x52,
|
||||
'S': 0x53,
|
||||
'T': 0x54,
|
||||
'U': 0x55,
|
||||
'V': 0x56,
|
||||
'W': 0x57,
|
||||
'X': 0x58,
|
||||
'Y': 0x59,
|
||||
'Z': 0x5A,
|
||||
# 0x5B is the small r in Dr Light
|
||||
'.': 0x5C,
|
||||
',': 0x5D,
|
||||
'\'': 0x5E,
|
||||
'!': 0x5F,
|
||||
'(': 0x60,
|
||||
')': 0x61,
|
||||
'#': 0x62,
|
||||
'$': 0x63,
|
||||
'%': 0x64,
|
||||
'&': 0x65,
|
||||
'*': 0x66,
|
||||
'+': 0x67,
|
||||
'/': 0x68,
|
||||
'\\': 0x69,
|
||||
':': 0x6A,
|
||||
';': 0x6B,
|
||||
'<': 0x6C,
|
||||
'>': 0x6D,
|
||||
'=': 0x6E,
|
||||
'?': 0x6F,
|
||||
'@': 0x70,
|
||||
'[': 0x71,
|
||||
']': 0x72,
|
||||
'^': 0x73,
|
||||
'_': 0x74,
|
||||
'`': 0x75,
|
||||
'{': 0x76,
|
||||
'}': 0x77,
|
||||
'|': 0x78,
|
||||
'~': 0x79,
|
||||
'\"': 0x92,
|
||||
'-': 0x94,
|
||||
'0': 0xA0,
|
||||
'1': 0xA1,
|
||||
'2': 0xA2,
|
||||
'3': 0xA3,
|
||||
'4': 0xA4,
|
||||
'5': 0xA5,
|
||||
'6': 0xA6,
|
||||
'7': 0xA7,
|
||||
'8': 0xA8,
|
||||
'9': 0xA9,
|
||||
})
|
||||
|
||||
|
||||
class MM2TextEntry:
|
||||
def __init__(self, text: str = "", coords: int = 0x0B):
|
||||
self.target_area: int = 0x25 # don't change
|
||||
self.coords: int = coords # 0xYX, Y can only be increments of 0x20
|
||||
self.text: str = text
|
||||
|
||||
def resolve(self) -> bytes:
|
||||
data = bytearray()
|
||||
data.append(self.target_area)
|
||||
data.append(self.coords)
|
||||
data.extend([MM2_WEAPON_ENCODING[x] for x in self.text.upper()])
|
||||
data.extend([0x40] * (14 - len(self.text)))
|
||||
return bytes(data)
|
||||
@@ -46,6 +46,8 @@ class MuseDashCollections:
|
||||
"CHAOS Glitch",
|
||||
"FM 17314 SUGAR RADIO",
|
||||
"Yume Ou Mono Yo Secret",
|
||||
"Echo over you... Secret",
|
||||
"Tsukuyomi Ni Naru Replaced",
|
||||
]
|
||||
|
||||
album_items: Dict[str, AlbumData] = {}
|
||||
|
||||
@@ -553,7 +553,7 @@ NOVA|73-4|Happy Otaku Pack Vol.19|True|6|8|10|
|
||||
Heaven's Gradius|73-5|Happy Otaku Pack Vol.19|True|6|8|10|
|
||||
Ray Tuning|74-0|CHUNITHM COURSE MUSE|True|6|8|10|
|
||||
World Vanquisher|74-1|CHUNITHM COURSE MUSE|True|6|8|10|11
|
||||
Tsukuyomi Ni Naru|74-2|CHUNITHM COURSE MUSE|False|5|7|9|
|
||||
Tsukuyomi Ni Naru Replaced|74-2|CHUNITHM COURSE MUSE|True|5|7|9|
|
||||
The wheel to the right|74-3|CHUNITHM COURSE MUSE|True|5|7|9|11
|
||||
Climax|74-4|CHUNITHM COURSE MUSE|True|4|8|11|11
|
||||
Spider's Thread|74-5|CHUNITHM COURSE MUSE|True|5|8|10|12
|
||||
@@ -567,3 +567,16 @@ SUPERHERO|75-0|Novice Rider Pack|False|2|4|7|
|
||||
Highway_Summer|75-1|Novice Rider Pack|True|2|4|6|
|
||||
Mx. Black Box|75-2|Novice Rider Pack|True|5|7|9|
|
||||
Sweet Encounter|75-3|Novice Rider Pack|True|2|4|7|
|
||||
Echo over you... Secret|0-55|Default Music|False|6|8|10|
|
||||
Echo over you...|0-56|Default Music|False|1|4|0|
|
||||
Tsukuyomi Ni Naru|74-6|CHUNITHM COURSE MUSE|True|5|8|10|
|
||||
disco light|76-0|MUSE RADIO FM105|True|5|7|9|
|
||||
room light feat.chancylemon|76-1|MUSE RADIO FM105|True|3|5|7|
|
||||
Invisible|76-2|MUSE RADIO FM105|True|3|5|8|
|
||||
Christmas Season-LLABB|76-3|MUSE RADIO FM105|True|1|4|7|
|
||||
Hyouryu|77-0|Let's Rhythm Jam!|False|6|8|10|
|
||||
The Whole Rest|77-1|Let's Rhythm Jam!|False|5|8|10|11
|
||||
Hydra|77-2|Let's Rhythm Jam!|False|4|7|11|
|
||||
Pastel Lines|77-3|Let's Rhythm Jam!|False|3|6|9|
|
||||
LINK x LIN#S|77-4|Let's Rhythm Jam!|False|3|6|9|
|
||||
Arcade ViruZ|77-5|Let's Rhythm Jam!|False|6|8|10|
|
||||
|
||||
@@ -445,7 +445,7 @@ def shuffle_random_entrances(ootworld):
|
||||
|
||||
# Gather locations to keep reachable for validation
|
||||
all_state = ootworld.get_state_with_complete_itempool()
|
||||
all_state.sweep_for_events(locations=ootworld.get_locations())
|
||||
all_state.sweep_for_advancements(locations=ootworld.get_locations())
|
||||
locations_to_ensure_reachable = {loc for loc in world.get_reachable_locations(all_state, player) if not (loc.type == 'Drop' or (loc.type == 'Event' and 'Subrule' in loc.name))}
|
||||
|
||||
# Set entrance data for all entrances
|
||||
@@ -791,8 +791,8 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all
|
||||
all_state = all_state_orig.copy()
|
||||
none_state = none_state_orig.copy()
|
||||
|
||||
all_state.sweep_for_events(locations=ootworld.get_locations())
|
||||
none_state.sweep_for_events(locations=ootworld.get_locations())
|
||||
all_state.sweep_for_advancements(locations=ootworld.get_locations())
|
||||
none_state.sweep_for_advancements(locations=ootworld.get_locations())
|
||||
|
||||
if ootworld.shuffle_interior_entrances or ootworld.shuffle_overworld_entrances or ootworld.spawn_positions:
|
||||
time_travel_state = none_state.copy()
|
||||
|
||||
@@ -228,7 +228,7 @@ def set_shop_rules(ootworld):
|
||||
def set_entrances_based_rules(ootworld):
|
||||
|
||||
all_state = ootworld.get_state_with_complete_itempool()
|
||||
all_state.sweep_for_events(locations=ootworld.get_locations())
|
||||
all_state.sweep_for_advancements(locations=ootworld.get_locations())
|
||||
|
||||
for location in filter(lambda location: location.type == 'Shop', ootworld.get_locations()):
|
||||
# If a shop is not reachable as adult, it can't have Goron Tunic or Zora Tunic as child can't buy these
|
||||
|
||||
@@ -847,7 +847,7 @@ class OOTWorld(World):
|
||||
# Make sure to only kill actual internal events, not in-game "events"
|
||||
all_state = self.get_state_with_complete_itempool()
|
||||
all_locations = self.get_locations()
|
||||
all_state.sweep_for_events(locations=all_locations)
|
||||
all_state.sweep_for_advancements(locations=all_locations)
|
||||
reachable = self.multiworld.get_reachable_locations(all_state, self.player)
|
||||
unreachable = [loc for loc in all_locations if
|
||||
(loc.internal or loc.type == 'Drop') and loc.address is None and loc.locked and loc not in reachable]
|
||||
@@ -875,7 +875,7 @@ class OOTWorld(World):
|
||||
state = base_state.copy()
|
||||
for item in self.get_pre_fill_items():
|
||||
self.collect(state, item)
|
||||
state.sweep_for_events(locations=self.get_locations())
|
||||
state.sweep_for_advancements(locations=self.get_locations())
|
||||
return state
|
||||
|
||||
# Prefill shops, songs, and dungeon items
|
||||
@@ -887,7 +887,7 @@ class OOTWorld(World):
|
||||
state = CollectionState(self.multiworld)
|
||||
for item in self.itempool:
|
||||
self.collect(state, item)
|
||||
state.sweep_for_events(locations=self.get_locations())
|
||||
state.sweep_for_advancements(locations=self.get_locations())
|
||||
|
||||
# Place dungeon items
|
||||
special_fill_types = ['GanonBossKey', 'BossKey', 'SmallKey', 'HideoutSmallKey', 'Map', 'Compass']
|
||||
|
||||
@@ -56,8 +56,8 @@ class OSRSWorld(World):
|
||||
|
||||
locations_by_category: typing.Dict[str, typing.List[LocationRow]]
|
||||
|
||||
def __init__(self, world: MultiWorld, player: int):
|
||||
super().__init__(world, player)
|
||||
def __init__(self, multiworld: MultiWorld, player: int):
|
||||
super().__init__(multiworld, player)
|
||||
self.region_name_to_data = {}
|
||||
self.location_name_to_data = {}
|
||||
|
||||
|
||||
@@ -137,6 +137,8 @@ class PokemonEmeraldClient(BizHawkClient):
|
||||
previous_death_link: float
|
||||
ignore_next_death_link: bool
|
||||
|
||||
current_map: Optional[int]
|
||||
|
||||
def __init__(self) -> None:
|
||||
super().__init__()
|
||||
self.local_checked_locations = set()
|
||||
@@ -150,6 +152,7 @@ class PokemonEmeraldClient(BizHawkClient):
|
||||
self.death_counter = None
|
||||
self.previous_death_link = 0
|
||||
self.ignore_next_death_link = False
|
||||
self.current_map = None
|
||||
|
||||
async def validate_rom(self, ctx: "BizHawkClientContext") -> bool:
|
||||
from CommonClient import logger
|
||||
@@ -243,6 +246,7 @@ class PokemonEmeraldClient(BizHawkClient):
|
||||
sb1_address = int.from_bytes(guards["SAVE BLOCK 1"][1], "little")
|
||||
sb2_address = int.from_bytes(guards["SAVE BLOCK 2"][1], "little")
|
||||
|
||||
await self.handle_tracker_info(ctx, guards)
|
||||
await self.handle_death_link(ctx, guards)
|
||||
await self.handle_received_items(ctx, guards)
|
||||
await self.handle_wonder_trade(ctx, guards)
|
||||
@@ -403,6 +407,30 @@ class PokemonEmeraldClient(BizHawkClient):
|
||||
# Exit handler and return to main loop to reconnect
|
||||
pass
|
||||
|
||||
async def handle_tracker_info(self, ctx: "BizHawkClientContext", guards: Dict[str, Tuple[int, bytes, str]]) -> None:
|
||||
# Current map
|
||||
sb1_address = int.from_bytes(guards["SAVE BLOCK 1"][1], "little")
|
||||
|
||||
read_result = await bizhawk.guarded_read(
|
||||
ctx.bizhawk_ctx,
|
||||
[(sb1_address + 0x4, 2, "System Bus")],
|
||||
[guards["SAVE BLOCK 1"]]
|
||||
)
|
||||
if read_result is None: # Save block moved
|
||||
return
|
||||
|
||||
current_map = int.from_bytes(read_result[0], "big")
|
||||
if current_map != self.current_map:
|
||||
self.current_map = current_map
|
||||
await ctx.send_msgs([{
|
||||
"cmd": "Bounce",
|
||||
"slots": [ctx.slot],
|
||||
"data": {
|
||||
"type": "MapUpdate",
|
||||
"mapId": current_map,
|
||||
},
|
||||
}])
|
||||
|
||||
async def handle_death_link(self, ctx: "BizHawkClientContext", guards: Dict[str, Tuple[int, bytes, str]]) -> None:
|
||||
"""
|
||||
Checks whether the player has died while connected and sends a death link if so. Queues a death link in the game
|
||||
|
||||
@@ -114,6 +114,14 @@ class PokemonEmeraldProcedurePatch(APProcedurePatch, APTokenMixin):
|
||||
|
||||
|
||||
def write_tokens(world: "PokemonEmeraldWorld", patch: PokemonEmeraldProcedurePatch) -> None:
|
||||
# TODO: Remove when the base patch is updated to include this change
|
||||
# Moves an NPC to avoid overlapping people during trainersanity
|
||||
patch.write_token(
|
||||
APTokenTypes.WRITE,
|
||||
0x53A298 + (0x18 * 7) + 4, # Space Center 1F event address + 8th event + 4-byte offset for x coord
|
||||
struct.pack("<H", 11)
|
||||
)
|
||||
|
||||
# Set free fly location
|
||||
if world.options.free_fly_location:
|
||||
patch.write_token(
|
||||
|
||||
@@ -296,7 +296,7 @@ class PokemonRedBlueWorld(World):
|
||||
if attempt > 1:
|
||||
for mon in poke_data.pokemon_data.keys():
|
||||
state.collect(self.create_item(mon), True)
|
||||
state.sweep_for_events()
|
||||
state.sweep_for_advancements()
|
||||
self.multiworld.random.shuffle(badges)
|
||||
self.multiworld.random.shuffle(badgelocs)
|
||||
badgelocs_copy = badgelocs.copy()
|
||||
|
||||
@@ -2439,7 +2439,7 @@ def door_shuffle(world, multiworld, player, badges, badge_locs):
|
||||
|
||||
state_copy = state.copy()
|
||||
state_copy.collect(item, True)
|
||||
state.sweep_for_events(locations=event_locations)
|
||||
state.sweep_for_advancements(locations=event_locations)
|
||||
new_reachable_entrances = len([entrance for entrance in entrances if entrance in reachable_entrances or
|
||||
entrance.parent_region.can_reach(state_copy)])
|
||||
return new_reachable_entrances > len(reachable_entrances)
|
||||
@@ -2480,7 +2480,7 @@ def door_shuffle(world, multiworld, player, badges, badge_locs):
|
||||
|
||||
while entrances:
|
||||
state.update_reachable_regions(player)
|
||||
state.sweep_for_events(locations=event_locations)
|
||||
state.sweep_for_advancements(locations=event_locations)
|
||||
|
||||
multiworld.random.shuffle(entrances)
|
||||
|
||||
|
||||
@@ -26,6 +26,7 @@ class FishItem:
|
||||
|
||||
fresh_water = (Region.farm, Region.forest, Region.town, Region.mountain)
|
||||
ocean = (Region.beach,)
|
||||
tide_pools = (Region.tide_pools,)
|
||||
town_river = (Region.town,)
|
||||
mountain_lake = (Region.mountain,)
|
||||
forest_pond = (Region.forest,)
|
||||
@@ -118,13 +119,13 @@ midnight_squid = create_fish(Fish.midnight_squid, night_market, season.winter, 5
|
||||
spook_fish = create_fish(Fish.spook_fish, night_market, season.winter, 60)
|
||||
|
||||
angler = create_fish(Fish.angler, town_river, season.fall, 85, True, False)
|
||||
crimsonfish = create_fish(Fish.crimsonfish, ocean, season.summer, 95, True, False)
|
||||
crimsonfish = create_fish(Fish.crimsonfish, tide_pools, season.summer, 95, True, False)
|
||||
glacierfish = create_fish(Fish.glacierfish, forest_river, season.winter, 100, True, False)
|
||||
legend = create_fish(Fish.legend, mountain_lake, season.spring, 110, True, False)
|
||||
mutant_carp = create_fish(Fish.mutant_carp, sewers, season.all_seasons, 80, True, False)
|
||||
|
||||
ms_angler = create_fish(Fish.ms_angler, town_river, season.fall, 85, True, True)
|
||||
son_of_crimsonfish = create_fish(Fish.son_of_crimsonfish, ocean, season.summer, 95, True, True)
|
||||
son_of_crimsonfish = create_fish(Fish.son_of_crimsonfish, tide_pools, season.summer, 95, True, True)
|
||||
glacierfish_jr = create_fish(Fish.glacierfish_jr, forest_river, season.winter, 100, True, True)
|
||||
legend_ii = create_fish(Fish.legend_ii, mountain_lake, season.spring, 110, True, True)
|
||||
radioactive_carp = create_fish(Fish.radioactive_carp, sewers, season.all_seasons, 80, True, True)
|
||||
|
||||
@@ -27,6 +27,7 @@ def collect_fishing_abilities(tester: SVTestBase):
|
||||
tester.multiworld.state.collect(tester.world.create_item("Fall"), prevent_sweep=False)
|
||||
tester.multiworld.state.collect(tester.world.create_item("Winter"), prevent_sweep=False)
|
||||
tester.multiworld.state.collect(tester.world.create_item(Transportation.desert_obelisk), prevent_sweep=False)
|
||||
tester.multiworld.state.collect(tester.world.create_item("Beach Bridge"), prevent_sweep=False)
|
||||
tester.multiworld.state.collect(tester.world.create_item("Railroad Boulder Removed"), prevent_sweep=False)
|
||||
tester.multiworld.state.collect(tester.world.create_item("Island North Turtle"), prevent_sweep=False)
|
||||
tester.multiworld.state.collect(tester.world.create_item("Island West Turtle"), prevent_sweep=False)
|
||||
|
||||
@@ -258,15 +258,19 @@ class SVTestBase(RuleAssertMixin, WorldTestBase, SVTestCase):
|
||||
|
||||
def collect_lots_of_money(self):
|
||||
self.multiworld.state.collect(self.world.create_item("Shipping Bin"), prevent_sweep=False)
|
||||
required_prog_items = int(round(self.multiworld.worlds[self.player].total_progression_items * 0.25))
|
||||
real_total_prog_items = self.multiworld.worlds[self.player].total_progression_items
|
||||
required_prog_items = int(round(real_total_prog_items * 0.25))
|
||||
for i in range(required_prog_items):
|
||||
self.multiworld.state.collect(self.world.create_item("Stardrop"), prevent_sweep=False)
|
||||
self.multiworld.worlds[self.player].total_progression_items = real_total_prog_items
|
||||
|
||||
def collect_all_the_money(self):
|
||||
self.multiworld.state.collect(self.world.create_item("Shipping Bin"), prevent_sweep=False)
|
||||
required_prog_items = int(round(self.multiworld.worlds[self.player].total_progression_items * 0.95))
|
||||
real_total_prog_items = self.multiworld.worlds[self.player].total_progression_items
|
||||
required_prog_items = int(round(real_total_prog_items * 0.95))
|
||||
for i in range(required_prog_items):
|
||||
self.multiworld.state.collect(self.world.create_item("Stardrop"), prevent_sweep=False)
|
||||
self.multiworld.worlds[self.player].total_progression_items = real_total_prog_items
|
||||
|
||||
def collect_everything(self):
|
||||
non_event_items = [item for item in self.multiworld.get_items() if item.code]
|
||||
|
||||
61
worlds/stardew_valley/test/rules/TestFishing.py
Normal file
61
worlds/stardew_valley/test/rules/TestFishing.py
Normal file
@@ -0,0 +1,61 @@
|
||||
from ...options import SeasonRandomization, Friendsanity, FriendsanityHeartSize, Fishsanity, ExcludeGingerIsland, SkillProgression, ToolProgression, \
|
||||
ElevatorProgression, SpecialOrderLocations
|
||||
from ...strings.fish_names import Fish
|
||||
from ...test import SVTestBase
|
||||
|
||||
|
||||
class TestNeedRegionToCatchFish(SVTestBase):
|
||||
options = {
|
||||
SeasonRandomization.internal_name: SeasonRandomization.option_disabled,
|
||||
ElevatorProgression.internal_name: ElevatorProgression.option_vanilla,
|
||||
SkillProgression.internal_name: SkillProgression.option_vanilla,
|
||||
ToolProgression.internal_name: ToolProgression.option_vanilla,
|
||||
Fishsanity.internal_name: Fishsanity.option_all,
|
||||
ExcludeGingerIsland.internal_name: ExcludeGingerIsland.option_false,
|
||||
SpecialOrderLocations.internal_name: SpecialOrderLocations.option_board_qi,
|
||||
}
|
||||
|
||||
def test_catch_fish_requires_region_unlock(self):
|
||||
fish_and_items = {
|
||||
Fish.crimsonfish: ["Beach Bridge"],
|
||||
Fish.void_salmon: ["Railroad Boulder Removed", "Dark Talisman"],
|
||||
Fish.woodskip: ["Glittering Boulder Removed", "Progressive Weapon"], # For the ores to get the axe upgrades
|
||||
Fish.mutant_carp: ["Rusty Key"],
|
||||
Fish.slimejack: ["Railroad Boulder Removed", "Rusty Key"],
|
||||
Fish.lionfish: ["Boat Repair"],
|
||||
Fish.blue_discus: ["Island Obelisk", "Island West Turtle"],
|
||||
Fish.stingray: ["Boat Repair", "Island Resort"],
|
||||
Fish.ghostfish: ["Progressive Weapon"],
|
||||
Fish.stonefish: ["Progressive Weapon"],
|
||||
Fish.ice_pip: ["Progressive Weapon", "Progressive Weapon"],
|
||||
Fish.lava_eel: ["Progressive Weapon", "Progressive Weapon", "Progressive Weapon"],
|
||||
Fish.sandfish: ["Bus Repair"],
|
||||
Fish.scorpion_carp: ["Desert Obelisk"],
|
||||
# Starting the extended family quest requires having caught all the legendaries before, so they all have the rules of every other legendary
|
||||
Fish.son_of_crimsonfish: ["Beach Bridge", "Island Obelisk", "Island West Turtle", "Qi Walnut Room", "Rusty Key"],
|
||||
Fish.radioactive_carp: ["Beach Bridge", "Rusty Key", "Boat Repair", "Island West Turtle", "Qi Walnut Room"],
|
||||
Fish.glacierfish_jr: ["Beach Bridge", "Island Obelisk", "Island West Turtle", "Qi Walnut Room", "Rusty Key"],
|
||||
Fish.legend_ii: ["Beach Bridge", "Island Obelisk", "Island West Turtle", "Qi Walnut Room", "Rusty Key"],
|
||||
Fish.ms_angler: ["Beach Bridge", "Island Obelisk", "Island West Turtle", "Qi Walnut Room", "Rusty Key"],
|
||||
}
|
||||
self.original_state = self.multiworld.state.copy()
|
||||
for fish in fish_and_items:
|
||||
with self.subTest(f"Region rules for {fish}"):
|
||||
self.collect_all_the_money()
|
||||
item_names = fish_and_items[fish]
|
||||
location = self.multiworld.get_location(f"Fishsanity: {fish}", self.player)
|
||||
self.assert_reach_location_false(location, self.multiworld.state)
|
||||
items = []
|
||||
for item_name in item_names:
|
||||
items.append(self.collect(item_name))
|
||||
with self.subTest(f"{fish} can be reached with {item_names}"):
|
||||
self.assert_reach_location_true(location, self.multiworld.state)
|
||||
for item_required in items:
|
||||
self.multiworld.state = self.original_state.copy()
|
||||
with self.subTest(f"{fish} requires {item_required.name}"):
|
||||
for item_to_collect in items:
|
||||
if item_to_collect.name != item_required.name:
|
||||
self.collect(item_to_collect)
|
||||
self.assert_reach_location_false(location, self.multiworld.state)
|
||||
|
||||
self.multiworld.state = self.original_state.copy()
|
||||
@@ -421,7 +421,8 @@ class TunicWorld(World):
|
||||
except KeyError:
|
||||
# logic bug, proceed with warning since it takes a long time to update AP
|
||||
warning(f"{location.name} is not logically accessible for {self.player_name}. "
|
||||
"Creating entrance hint Inaccessible. Please report this to the TUNIC rando devs.")
|
||||
"Creating entrance hint Inaccessible. Please report this to the TUNIC rando devs. "
|
||||
"If you are using Plando Items (excluding early locations), then this is likely the cause.")
|
||||
hint_text = "Inaccessible"
|
||||
else:
|
||||
while connection != ("Menu", None):
|
||||
|
||||
@@ -49,12 +49,14 @@ class V6World(World):
|
||||
self.area_cost_map = {}
|
||||
set_rules(self.multiworld, self.options, self.player, self.area_connections, self.area_cost_map)
|
||||
|
||||
def create_item(self, name: str) -> Item:
|
||||
return V6Item(name, ItemClassification.progression, item_table[name], self.player)
|
||||
def create_item(self, name: str, classification: ItemClassification = ItemClassification.filler) -> Item:
|
||||
return V6Item(name, classification, item_table[name], self.player)
|
||||
|
||||
def create_items(self):
|
||||
trinkets = [self.create_item("Trinket " + str(i+1).zfill(2)) for i in range(0,20)]
|
||||
self.multiworld.itempool += trinkets
|
||||
progtrinkets = [self.create_item("Trinket " + str(i+1).zfill(2), ItemClassification.progression) for i in range(0, (4 * self.options.door_cost.value))]
|
||||
filltrinkets = [self.create_item("Trinket " + str(i+1).zfill(2)) for i in range((4 * self.options.door_cost.value), 20)]
|
||||
self.multiworld.itempool += progtrinkets
|
||||
self.multiworld.itempool += filltrinkets
|
||||
|
||||
def generate_basic(self):
|
||||
musiclist_o = [1,2,3,4,9,12]
|
||||
|
||||
@@ -15,7 +15,7 @@ from .data import static_locations as static_witness_locations
|
||||
from .data import static_logic as static_witness_logic
|
||||
from .data.item_definition_classes import DoorItemDefinition, ItemData
|
||||
from .data.utils import get_audio_logs
|
||||
from .hints import CompactItemData, create_all_hints, make_compact_hint_data, make_laser_hints
|
||||
from .hints import CompactHintData, create_all_hints, make_compact_hint_data, make_laser_hints
|
||||
from .locations import WitnessPlayerLocations
|
||||
from .options import TheWitnessOptions, witness_option_groups
|
||||
from .player_items import WitnessItem, WitnessPlayerItems
|
||||
@@ -68,12 +68,14 @@ class WitnessWorld(World):
|
||||
player_items: WitnessPlayerItems
|
||||
player_regions: WitnessPlayerRegions
|
||||
|
||||
log_ids_to_hints: Dict[int, CompactItemData]
|
||||
laser_ids_to_hints: Dict[int, CompactItemData]
|
||||
log_ids_to_hints: Dict[int, CompactHintData]
|
||||
laser_ids_to_hints: Dict[int, CompactHintData]
|
||||
|
||||
items_placed_early: List[str]
|
||||
own_itempool: List[WitnessItem]
|
||||
|
||||
panel_hunt_required_count: int
|
||||
|
||||
def _get_slot_data(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"seed": self.random.randrange(0, 1000000),
|
||||
@@ -83,12 +85,14 @@ class WitnessWorld(World):
|
||||
"door_hexes_in_the_pool": self.player_items.get_door_ids_in_pool(),
|
||||
"symbols_not_in_the_game": self.player_items.get_symbol_ids_not_in_pool(),
|
||||
"disabled_entities": [int(h, 16) for h in self.player_logic.COMPLETELY_DISABLED_ENTITIES],
|
||||
"hunt_entities": [int(h, 16) for h in self.player_logic.HUNT_ENTITIES],
|
||||
"log_ids_to_hints": self.log_ids_to_hints,
|
||||
"laser_ids_to_hints": self.laser_ids_to_hints,
|
||||
"progressive_item_lists": self.player_items.get_progressive_item_ids_in_pool(),
|
||||
"obelisk_side_id_to_EPs": static_witness_logic.OBELISK_SIDE_ID_TO_EP_HEXES,
|
||||
"precompleted_puzzles": [int(h, 16) for h in self.player_logic.EXCLUDED_LOCATIONS],
|
||||
"precompleted_puzzles": [int(h, 16) for h in self.player_logic.EXCLUDED_ENTITIES],
|
||||
"entity_to_name": static_witness_logic.ENTITY_ID_TO_NAME,
|
||||
"panel_hunt_required_absolute": self.panel_hunt_required_count
|
||||
}
|
||||
|
||||
def determine_sufficient_progression(self) -> None:
|
||||
@@ -124,10 +128,10 @@ class WitnessWorld(World):
|
||||
)
|
||||
|
||||
if not has_locally_relevant_progression and self.multiworld.players == 1:
|
||||
warning(f"{self.multiworld.get_player_name(self.player)}'s Witness world doesn't have any progression"
|
||||
warning(f"{self.player_name}'s Witness world doesn't have any progression"
|
||||
f" items. Please turn on Symbol Shuffle, Door Shuffle or Laser Shuffle if that doesn't seem right.")
|
||||
elif not interacts_sufficiently_with_multiworld and self.multiworld.players > 1:
|
||||
raise OptionError(f"{self.multiworld.get_player_name(self.player)}'s Witness world doesn't have enough"
|
||||
raise OptionError(f"{self.player_name}'s Witness world doesn't have enough"
|
||||
f" progression items that can be placed in other players' worlds. Please turn on Symbol"
|
||||
f" Shuffle, Door Shuffle, or Obelisk Keys.")
|
||||
|
||||
@@ -151,6 +155,13 @@ class WitnessWorld(World):
|
||||
if self.options.shuffle_lasers == "local":
|
||||
self.options.local_items.value |= self.item_name_groups["Lasers"]
|
||||
|
||||
if self.options.victory_condition == "panel_hunt":
|
||||
total_panels = self.options.panel_hunt_total
|
||||
required_percentage = self.options.panel_hunt_required_percentage
|
||||
self.panel_hunt_required_count = round(total_panels * required_percentage / 100)
|
||||
else:
|
||||
self.panel_hunt_required_count = 0
|
||||
|
||||
def create_regions(self) -> None:
|
||||
self.player_regions.create_regions(self, self.player_logic)
|
||||
|
||||
@@ -169,7 +180,7 @@ class WitnessWorld(World):
|
||||
|
||||
for event_location in self.player_locations.EVENT_LOCATION_TABLE:
|
||||
item_obj = self.create_item(
|
||||
self.player_logic.EVENT_ITEM_PAIRS[event_location]
|
||||
self.player_logic.EVENT_ITEM_PAIRS[event_location][0]
|
||||
)
|
||||
location_obj = self.get_location(event_location)
|
||||
location_obj.place_locked_item(item_obj)
|
||||
@@ -178,12 +189,13 @@ class WitnessWorld(World):
|
||||
event_locations.append(location_obj)
|
||||
|
||||
# Place other locked items
|
||||
dog_puzzle_skip = self.create_item("Puzzle Skip")
|
||||
self.get_location("Town Pet the Dog").place_locked_item(dog_puzzle_skip)
|
||||
|
||||
self.own_itempool.append(dog_puzzle_skip)
|
||||
if self.options.shuffle_dog == "puzzle_skip":
|
||||
dog_puzzle_skip = self.create_item("Puzzle Skip")
|
||||
self.get_location("Town Pet the Dog").place_locked_item(dog_puzzle_skip)
|
||||
|
||||
self.items_placed_early.append("Puzzle Skip")
|
||||
self.own_itempool.append(dog_puzzle_skip)
|
||||
self.items_placed_early.append("Puzzle Skip")
|
||||
|
||||
if self.options.early_symbol_item:
|
||||
# Pick an early item to place on the tutorial gate.
|
||||
@@ -192,8 +204,11 @@ class WitnessWorld(World):
|
||||
]
|
||||
if early_items:
|
||||
random_early_item = self.random.choice(early_items)
|
||||
if self.options.puzzle_randomization == "sigma_expert":
|
||||
# In Expert, only tag the item as early, rather than forcing it onto the gate.
|
||||
if (
|
||||
self.options.puzzle_randomization == "sigma_expert"
|
||||
or self.options.victory_condition == "panel_hunt"
|
||||
):
|
||||
# In Expert and Panel Hunt, only tag the item as early, rather than forcing it onto the gate.
|
||||
self.multiworld.local_early_items[self.player][random_early_item] = 1
|
||||
else:
|
||||
# Force the item onto the tutorial gate check and remove it from our random pool.
|
||||
@@ -202,19 +217,22 @@ class WitnessWorld(World):
|
||||
self.own_itempool.append(gate_item)
|
||||
self.items_placed_early.append(random_early_item)
|
||||
|
||||
# There are some really restrictive settings in The Witness.
|
||||
# There are some really restrictive options in The Witness.
|
||||
# They are rarely played, but when they are, we add some extra sphere 1 locations.
|
||||
# This is done both to prevent generation failures, but also to make the early game less linear.
|
||||
# Only sweeps for events because having this behavior be random based on Tutorial Gate would be strange.
|
||||
|
||||
state = CollectionState(self.multiworld)
|
||||
state.sweep_for_events(locations=event_locations)
|
||||
state.sweep_for_advancements(locations=event_locations)
|
||||
|
||||
num_early_locs = sum(1 for loc in self.multiworld.get_reachable_locations(state, self.player) if loc.address)
|
||||
num_early_locs = sum(
|
||||
1 for loc in self.multiworld.get_reachable_locations(state, self.player)
|
||||
if loc.address and not loc.item
|
||||
)
|
||||
|
||||
# Adjust the needed size for sphere 1 based on how restrictive the settings are in terms of items
|
||||
# Adjust the needed size for sphere 1 based on how restrictive the options are in terms of items
|
||||
|
||||
needed_size = 3
|
||||
needed_size = 2
|
||||
needed_size += self.options.puzzle_randomization == "sigma_expert"
|
||||
needed_size += self.options.shuffle_symbols
|
||||
needed_size += self.options.shuffle_doors > 0
|
||||
@@ -236,9 +254,10 @@ class WitnessWorld(World):
|
||||
self.player_locations.add_location_late(loc)
|
||||
self.get_region(region).add_locations({loc: self.location_name_to_id[loc]})
|
||||
|
||||
player = self.multiworld.get_player_name(self.player)
|
||||
|
||||
warning(f"""Location "{loc}" had to be added to {player}'s world due to insufficient sphere 1 size.""")
|
||||
warning(
|
||||
f"""Location "{loc}" had to be added to {self.player_name}'s world
|
||||
due to insufficient sphere 1 size."""
|
||||
)
|
||||
|
||||
def create_items(self) -> None:
|
||||
# Determine pool size.
|
||||
@@ -275,7 +294,7 @@ class WitnessWorld(World):
|
||||
self.multiworld.push_precollected(self.create_item(inventory_item_name))
|
||||
|
||||
if len(item_pool) > pool_size:
|
||||
error(f"{self.multiworld.get_player_name(self.player)}'s Witness world has too few locations ({pool_size})"
|
||||
error(f"{self.player_name}'s Witness world has too few locations ({pool_size})"
|
||||
f" to place its necessary items ({len(item_pool)}).")
|
||||
return
|
||||
|
||||
@@ -285,7 +304,7 @@ class WitnessWorld(World):
|
||||
num_puzzle_skips = self.options.puzzle_skip_amount.value
|
||||
|
||||
if num_puzzle_skips > remaining_item_slots:
|
||||
warning(f"{self.multiworld.get_player_name(self.player)}'s Witness world has insufficient locations"
|
||||
warning(f"{self.player_name}'s Witness world has insufficient locations"
|
||||
f" to place all requested puzzle skips.")
|
||||
num_puzzle_skips = remaining_item_slots
|
||||
item_pool["Puzzle Skip"] = num_puzzle_skips
|
||||
@@ -305,8 +324,8 @@ class WitnessWorld(World):
|
||||
self.options.local_items.value.add(item_name)
|
||||
|
||||
def fill_slot_data(self) -> Dict[str, Any]:
|
||||
self.log_ids_to_hints: Dict[int, CompactItemData] = {}
|
||||
self.laser_ids_to_hints: Dict[int, CompactItemData] = {}
|
||||
self.log_ids_to_hints: Dict[int, CompactHintData] = {}
|
||||
self.laser_ids_to_hints: Dict[int, CompactHintData] = {}
|
||||
|
||||
already_hinted_locations = set()
|
||||
|
||||
|
||||
@@ -176,6 +176,7 @@ Door - 0x03444 (Vault Door) - 0x0CC7B
|
||||
Door - 0x09FEE (Light Room Entry) - 0x0C339
|
||||
158701 - 0x03608 (Laser Panel) - 0x012D7 & 0x0A15F - True
|
||||
Laser - 0x012FB (Laser) - 0x03608
|
||||
159804 - 0xFFD03 (Laser Activated + Redirected) - 0x09F98 & 0x012FB - True
|
||||
159020 - 0x3351D (Sand Snake EP) - True - True
|
||||
159030 - 0x0053C (Facade Right EP) - True - True
|
||||
159031 - 0x00771 (Facade Left EP) - True - True
|
||||
@@ -980,7 +981,7 @@ Mountainside Obelisk (Mountainside) - Entry - True:
|
||||
159739 - 0x00367 (Obelisk) - True - True
|
||||
|
||||
Mountainside (Mountainside) - Main Island - True - Mountaintop - True - Mountainside Vault - 0x00085:
|
||||
159550 - 0x28B91 (Thundercloud EP) - 0x09F98 & 0x012FB - True
|
||||
159550 - 0x28B91 (Thundercloud EP) - 0xFFD03 - True
|
||||
158612 - 0x17C42 (Discard) - True - Triangles
|
||||
158665 - 0x002A6 (Vault Panel) - True - Symmetry & Colored Dots & Black/White Squares & Dots
|
||||
Door - 0x00085 (Vault Door) - 0x002A6
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user