mirror of
https://github.com/ArchipelagoMW/Archipelago.git
synced 2026-05-27 01:49:55 -07:00
Merge branch 'main' into feature/ds3_use_slotdata
# Conflicts: # worlds/dark_souls_3/__init__.py
This commit is contained in:
@@ -0,0 +1,42 @@
|
||||
|
||||
from __future__ import annotations
|
||||
import abc
|
||||
from typing import TYPE_CHECKING, ClassVar, Dict, Tuple, Any, Optional
|
||||
|
||||
if TYPE_CHECKING:
|
||||
from SNIClient import SNIContext
|
||||
|
||||
|
||||
class AutoSNIClientRegister(abc.ABCMeta):
|
||||
game_handlers: ClassVar[Dict[str, SNIClient]] = {}
|
||||
|
||||
def __new__(cls, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> AutoSNIClientRegister:
|
||||
# construct class
|
||||
new_class = super().__new__(cls, name, bases, dct)
|
||||
if "game" in dct:
|
||||
AutoSNIClientRegister.game_handlers[dct["game"]] = new_class()
|
||||
return new_class
|
||||
|
||||
@staticmethod
|
||||
async def get_handler(ctx: SNIContext) -> Optional[SNIClient]:
|
||||
for _game, handler in AutoSNIClientRegister.game_handlers.items():
|
||||
if await handler.validate_rom(ctx):
|
||||
return handler
|
||||
return None
|
||||
|
||||
|
||||
class SNIClient(abc.ABC, metaclass=AutoSNIClientRegister):
|
||||
|
||||
@abc.abstractmethod
|
||||
async def validate_rom(self, ctx: SNIContext) -> bool:
|
||||
""" TODO: interface documentation here """
|
||||
...
|
||||
|
||||
@abc.abstractmethod
|
||||
async def game_watcher(self, ctx: SNIContext) -> None:
|
||||
""" TODO: interface documentation here """
|
||||
...
|
||||
|
||||
async def deathlink_kill_player(self, ctx: SNIContext) -> None:
|
||||
""" override this with implementation to kill player """
|
||||
pass
|
||||
+22
-6
@@ -3,9 +3,9 @@ from __future__ import annotations
|
||||
import logging
|
||||
import sys
|
||||
import pathlib
|
||||
from typing import Dict, FrozenSet, Set, Tuple, List, Optional, TextIO, Any, Callable, Union, TYPE_CHECKING
|
||||
from typing import Dict, FrozenSet, Set, Tuple, List, Optional, TextIO, Any, Callable, Type, Union, TYPE_CHECKING
|
||||
|
||||
from Options import Option
|
||||
from Options import AssembleOptions
|
||||
from BaseClasses import CollectionState
|
||||
|
||||
if TYPE_CHECKING:
|
||||
@@ -13,7 +13,7 @@ if TYPE_CHECKING:
|
||||
|
||||
|
||||
class AutoWorldRegister(type):
|
||||
world_types: Dict[str, type(World)] = {}
|
||||
world_types: Dict[str, Type[World]] = {}
|
||||
|
||||
def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> AutoWorldRegister:
|
||||
if "web" in dct:
|
||||
@@ -79,8 +79,16 @@ def call_single(world: "MultiWorld", method_name: str, player: int, *args: Any)
|
||||
def call_all(world: "MultiWorld", method_name: str, *args: Any) -> None:
|
||||
world_types: Set[AutoWorldRegister] = set()
|
||||
for player in world.player_ids:
|
||||
prev_item_count = len(world.itempool)
|
||||
world_types.add(world.worlds[player].__class__)
|
||||
call_single(world, method_name, player, *args)
|
||||
if __debug__:
|
||||
new_items = world.itempool[prev_item_count:]
|
||||
for i, item in enumerate(new_items):
|
||||
for other in new_items[i+1:]:
|
||||
assert item is not other, (
|
||||
f"Duplicate item reference of \"{item.name}\" in \"{world.worlds[player].game}\" "
|
||||
f"of player \"{world.player_name[player]}\". Please make a copy instead.")
|
||||
|
||||
for world_type in world_types:
|
||||
stage_callable = getattr(world_type, f"stage_{method_name}", None)
|
||||
@@ -120,7 +128,7 @@ class World(metaclass=AutoWorldRegister):
|
||||
"""A World object encompasses a game's Items, Locations, Rules and additional data or functionality required.
|
||||
A Game should have its own subclass of World in which it defines the required data structures."""
|
||||
|
||||
option_definitions: Dict[str, Option[Any]] = {} # link your Options mapping
|
||||
option_definitions: Dict[str, AssembleOptions] = {} # link your Options mapping
|
||||
game: str # name the game
|
||||
topology_present: bool = False # indicate if world type has any meaningful layout/pathing
|
||||
|
||||
@@ -229,7 +237,8 @@ class World(metaclass=AutoWorldRegister):
|
||||
pass
|
||||
|
||||
def post_fill(self) -> None:
|
||||
"""Optional Method that is called after regular fill. Can be used to do adjustments before output generation."""
|
||||
"""Optional Method that is called after regular fill. Can be used to do adjustments before output generation.
|
||||
This happens before progression balancing, so the items may not be in their final locations yet."""
|
||||
|
||||
def generate_output(self, output_directory: str) -> None:
|
||||
"""This method gets called from a threadpool, do not use world.random here.
|
||||
@@ -237,9 +246,16 @@ class World(metaclass=AutoWorldRegister):
|
||||
pass
|
||||
|
||||
def fill_slot_data(self) -> Dict[str, Any]: # json of WebHostLib.models.Slot
|
||||
"""Fill in the slot_data field in the Connected network package."""
|
||||
"""Fill in the `slot_data` field in the `Connected` network package.
|
||||
This is a way the generator can give custom data to the client.
|
||||
The client will receive this as JSON in the `Connected` response."""
|
||||
return {}
|
||||
|
||||
def extend_hint_information(self, hint_data: Dict[int, Dict[int, str]]):
|
||||
"""Fill in additional entrance information text into locations, which is displayed when hinted.
|
||||
structure is {player_id: {location_id: text}} You will need to insert your own player_id."""
|
||||
pass
|
||||
|
||||
def modify_multidata(self, multidata: Dict[str, Any]) -> None: # TODO: TypedDict for multidata?
|
||||
"""For deeper modification of server multidata."""
|
||||
pass
|
||||
|
||||
+156
@@ -0,0 +1,156 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import zipfile
|
||||
|
||||
from typing import ClassVar, Dict, Tuple, Any, Optional, Union, BinaryIO
|
||||
|
||||
import bsdiff4
|
||||
|
||||
|
||||
class AutoPatchRegister(type):
|
||||
patch_types: ClassVar[Dict[str, AutoPatchRegister]] = {}
|
||||
file_endings: ClassVar[Dict[str, AutoPatchRegister]] = {}
|
||||
|
||||
def __new__(mcs, name: str, bases: Tuple[type, ...], dct: Dict[str, Any]) -> AutoPatchRegister:
|
||||
# construct class
|
||||
new_class = super().__new__(mcs, name, bases, dct)
|
||||
if "game" in dct:
|
||||
AutoPatchRegister.patch_types[dct["game"]] = new_class
|
||||
if not dct["patch_file_ending"]:
|
||||
raise Exception(f"Need an expected file ending for {name}")
|
||||
AutoPatchRegister.file_endings[dct["patch_file_ending"]] = new_class
|
||||
return new_class
|
||||
|
||||
@staticmethod
|
||||
def get_handler(file: str) -> Optional[AutoPatchRegister]:
|
||||
for file_ending, handler in AutoPatchRegister.file_endings.items():
|
||||
if file.endswith(file_ending):
|
||||
return handler
|
||||
return None
|
||||
|
||||
|
||||
current_patch_version: int = 5
|
||||
|
||||
|
||||
class APContainer:
|
||||
"""A zipfile containing at least archipelago.json"""
|
||||
version: int = current_patch_version
|
||||
compression_level: int = 9
|
||||
compression_method: int = zipfile.ZIP_DEFLATED
|
||||
game: Optional[str] = None
|
||||
|
||||
# instance attributes:
|
||||
path: Optional[str]
|
||||
player: Optional[int]
|
||||
player_name: str
|
||||
server: str
|
||||
|
||||
def __init__(self, path: Optional[str] = None, player: Optional[int] = None,
|
||||
player_name: str = "", server: str = ""):
|
||||
self.path = path
|
||||
self.player = player
|
||||
self.player_name = player_name
|
||||
self.server = server
|
||||
|
||||
def write(self, file: Optional[Union[str, BinaryIO]] = None) -> None:
|
||||
zip_file = file if file else self.path
|
||||
if not zip_file:
|
||||
raise FileNotFoundError(f"Cannot write {self.__class__.__name__} due to no path provided.")
|
||||
with zipfile.ZipFile(zip_file, "w", self.compression_method, True, self.compression_level) \
|
||||
as zf:
|
||||
if file:
|
||||
self.path = zf.filename
|
||||
self.write_contents(zf)
|
||||
|
||||
def write_contents(self, opened_zipfile: zipfile.ZipFile) -> None:
|
||||
manifest = self.get_manifest()
|
||||
try:
|
||||
manifest_str = json.dumps(manifest)
|
||||
except Exception as e:
|
||||
raise Exception(f"Manifest {manifest} did not convert to json.") from e
|
||||
else:
|
||||
opened_zipfile.writestr("archipelago.json", manifest_str)
|
||||
|
||||
def read(self, file: Optional[Union[str, BinaryIO]] = None) -> None:
|
||||
"""Read data into patch object. file can be file-like, such as an outer zip file's stream."""
|
||||
zip_file = file if file else self.path
|
||||
if not zip_file:
|
||||
raise FileNotFoundError(f"Cannot read {self.__class__.__name__} due to no path provided.")
|
||||
with zipfile.ZipFile(zip_file, "r") as zf:
|
||||
if file:
|
||||
self.path = zf.filename
|
||||
self.read_contents(zf)
|
||||
|
||||
def read_contents(self, opened_zipfile: zipfile.ZipFile) -> None:
|
||||
with opened_zipfile.open("archipelago.json", "r") as f:
|
||||
manifest = json.load(f)
|
||||
if manifest["compatible_version"] > self.version:
|
||||
raise Exception(f"File (version: {manifest['compatible_version']}) too new "
|
||||
f"for this handler (version: {self.version})")
|
||||
self.player = manifest["player"]
|
||||
self.server = manifest["server"]
|
||||
self.player_name = manifest["player_name"]
|
||||
|
||||
def get_manifest(self) -> Dict[str, Any]:
|
||||
return {
|
||||
"server": self.server, # allow immediate connection to server in multiworld. Empty string otherwise
|
||||
"player": self.player,
|
||||
"player_name": self.player_name,
|
||||
"game": self.game,
|
||||
# minimum version of patch system expected for patching to be successful
|
||||
"compatible_version": 5,
|
||||
"version": current_patch_version,
|
||||
}
|
||||
|
||||
|
||||
class APDeltaPatch(APContainer, metaclass=AutoPatchRegister):
|
||||
"""An APContainer that additionally has delta.bsdiff4
|
||||
containing a delta patch to get the desired file, often a rom."""
|
||||
|
||||
hash: Optional[str] # base checksum of source file
|
||||
patch_file_ending: str = ""
|
||||
delta: Optional[bytes] = None
|
||||
result_file_ending: str = ".sfc"
|
||||
source_data: bytes
|
||||
|
||||
def __init__(self, *args: Any, patched_path: str = "", **kwargs: Any) -> None:
|
||||
self.patched_path = patched_path
|
||||
super(APDeltaPatch, self).__init__(*args, **kwargs)
|
||||
|
||||
def get_manifest(self) -> Dict[str, Any]:
|
||||
manifest = super(APDeltaPatch, self).get_manifest()
|
||||
manifest["base_checksum"] = self.hash
|
||||
manifest["result_file_ending"] = self.result_file_ending
|
||||
manifest["patch_file_ending"] = self.patch_file_ending
|
||||
return manifest
|
||||
|
||||
@classmethod
|
||||
def get_source_data(cls) -> bytes:
|
||||
"""Get Base data"""
|
||||
raise NotImplementedError()
|
||||
|
||||
@classmethod
|
||||
def get_source_data_with_cache(cls) -> bytes:
|
||||
if not hasattr(cls, "source_data"):
|
||||
cls.source_data = cls.get_source_data()
|
||||
return cls.source_data
|
||||
|
||||
def write_contents(self, opened_zipfile: zipfile.ZipFile):
|
||||
super(APDeltaPatch, self).write_contents(opened_zipfile)
|
||||
# write Delta
|
||||
opened_zipfile.writestr("delta.bsdiff4",
|
||||
bsdiff4.diff(self.get_source_data_with_cache(), open(self.patched_path, "rb").read()),
|
||||
compress_type=zipfile.ZIP_STORED) # bsdiff4 is a format with integrated compression
|
||||
|
||||
def read_contents(self, opened_zipfile: zipfile.ZipFile):
|
||||
super(APDeltaPatch, self).read_contents(opened_zipfile)
|
||||
self.delta = opened_zipfile.read("delta.bsdiff4")
|
||||
|
||||
def patch(self, target: str):
|
||||
"""Base + Delta -> Patched"""
|
||||
if not self.delta:
|
||||
self.read()
|
||||
result = bsdiff4.patch(self.get_source_data_with_cache(), self.delta)
|
||||
with open(target, "wb") as f:
|
||||
f.write(result)
|
||||
+13
-3
@@ -1,7 +1,9 @@
|
||||
import importlib
|
||||
import zipimport
|
||||
import os
|
||||
import sys
|
||||
import typing
|
||||
import warnings
|
||||
import zipimport
|
||||
|
||||
folder = os.path.dirname(__file__)
|
||||
|
||||
@@ -27,7 +29,8 @@ class WorldSource(typing.NamedTuple):
|
||||
world_sources: typing.List[WorldSource] = []
|
||||
file: os.DirEntry # for me (Berserker) at least, PyCharm doesn't seem to infer the type correctly
|
||||
for file in os.scandir(folder):
|
||||
if not file.name.startswith("_"): # prevent explicitly loading __pycache__ and allow _* names for non-world folders
|
||||
# prevent loading of __pycache__ and allow _* for non-world folders, disable files/folders starting with "."
|
||||
if not file.name.startswith(("_", ".")):
|
||||
if file.is_dir():
|
||||
world_sources.append(WorldSource(file.name))
|
||||
elif file.is_file() and file.name.endswith(".apworld"):
|
||||
@@ -38,7 +41,14 @@ world_sources.sort()
|
||||
for world_source in world_sources:
|
||||
if world_source.is_zip:
|
||||
importer = zipimport.zipimporter(os.path.join(folder, world_source.path))
|
||||
importer.load_module(world_source.path.split(".", 1)[0])
|
||||
spec = importer.find_spec(world_source.path.split(".", 1)[0])
|
||||
mod = importlib.util.module_from_spec(spec)
|
||||
mod.__package__ = f"worlds.{mod.__package__}"
|
||||
mod.__name__ = f"worlds.{mod.__name__}"
|
||||
sys.modules[mod.__name__] = mod
|
||||
with warnings.catch_warnings():
|
||||
warnings.filterwarnings("ignore", message="__package__ != __spec__.parent")
|
||||
importer.exec_module(mod)
|
||||
else:
|
||||
importlib.import_module(f".{world_source.path}", "worlds")
|
||||
|
||||
|
||||
@@ -3,7 +3,7 @@ from typing import Optional, Union, List, Tuple, Callable, Dict
|
||||
|
||||
from BaseClasses import Boss
|
||||
from Fill import FillError
|
||||
from .Options import Bosses
|
||||
from .Options import LTTPBosses as Bosses
|
||||
|
||||
|
||||
def BossFactory(boss: str, player: int) -> Optional[Boss]:
|
||||
|
||||
@@ -0,0 +1,693 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import logging
|
||||
import asyncio
|
||||
import shutil
|
||||
import time
|
||||
|
||||
import Utils
|
||||
|
||||
from NetUtils import ClientStatus, color
|
||||
from worlds.AutoSNIClient import SNIClient
|
||||
|
||||
from worlds.alttp import Shops, Regions
|
||||
from .Rom import ROM_PLAYER_LIMIT
|
||||
|
||||
snes_logger = logging.getLogger("SNES")
|
||||
|
||||
GAME_ALTTP = "A Link to the Past"
|
||||
|
||||
# FXPAK Pro protocol memory mapping used by SNI
|
||||
ROM_START = 0x000000
|
||||
WRAM_START = 0xF50000
|
||||
WRAM_SIZE = 0x20000
|
||||
SRAM_START = 0xE00000
|
||||
|
||||
ROMNAME_START = SRAM_START + 0x2000
|
||||
ROMNAME_SIZE = 0x15
|
||||
|
||||
INGAME_MODES = {0x07, 0x09, 0x0b}
|
||||
ENDGAME_MODES = {0x19, 0x1a}
|
||||
DEATH_MODES = {0x12}
|
||||
|
||||
SAVEDATA_START = WRAM_START + 0xF000
|
||||
SAVEDATA_SIZE = 0x500
|
||||
|
||||
RECV_PROGRESS_ADDR = SAVEDATA_START + 0x4D0 # 2 bytes
|
||||
RECV_ITEM_ADDR = SAVEDATA_START + 0x4D2 # 1 byte
|
||||
RECV_ITEM_PLAYER_ADDR = SAVEDATA_START + 0x4D3 # 1 byte
|
||||
ROOMID_ADDR = SAVEDATA_START + 0x4D4 # 2 bytes
|
||||
ROOMDATA_ADDR = SAVEDATA_START + 0x4D6 # 1 byte
|
||||
SCOUT_LOCATION_ADDR = SAVEDATA_START + 0x4D7 # 1 byte
|
||||
SCOUTREPLY_LOCATION_ADDR = SAVEDATA_START + 0x4D8 # 1 byte
|
||||
SCOUTREPLY_ITEM_ADDR = SAVEDATA_START + 0x4D9 # 1 byte
|
||||
SCOUTREPLY_PLAYER_ADDR = SAVEDATA_START + 0x4DA # 1 byte
|
||||
SHOP_ADDR = SAVEDATA_START + 0x302 # 2 bytes
|
||||
SHOP_LEN = (len(Shops.shop_table) * 3) + 5
|
||||
|
||||
DEATH_LINK_ACTIVE_ADDR = ROMNAME_START + 0x15 # 1 byte
|
||||
|
||||
location_shop_ids = set([info[0] for name, info in Shops.shop_table.items()])
|
||||
|
||||
location_table_uw = {"Blind's Hideout - Top": (0x11d, 0x10),
|
||||
"Blind's Hideout - Left": (0x11d, 0x20),
|
||||
"Blind's Hideout - Right": (0x11d, 0x40),
|
||||
"Blind's Hideout - Far Left": (0x11d, 0x80),
|
||||
"Blind's Hideout - Far Right": (0x11d, 0x100),
|
||||
'Secret Passage': (0x55, 0x10),
|
||||
'Waterfall Fairy - Left': (0x114, 0x10),
|
||||
'Waterfall Fairy - Right': (0x114, 0x20),
|
||||
"King's Tomb": (0x113, 0x10),
|
||||
'Floodgate Chest': (0x10b, 0x10),
|
||||
"Link's House": (0x104, 0x10),
|
||||
'Kakariko Tavern': (0x103, 0x10),
|
||||
'Chicken House': (0x108, 0x10),
|
||||
"Aginah's Cave": (0x10a, 0x10),
|
||||
"Sahasrahla's Hut - Left": (0x105, 0x10),
|
||||
"Sahasrahla's Hut - Middle": (0x105, 0x20),
|
||||
"Sahasrahla's Hut - Right": (0x105, 0x40),
|
||||
'Kakariko Well - Top': (0x2f, 0x10),
|
||||
'Kakariko Well - Left': (0x2f, 0x20),
|
||||
'Kakariko Well - Middle': (0x2f, 0x40),
|
||||
'Kakariko Well - Right': (0x2f, 0x80),
|
||||
'Kakariko Well - Bottom': (0x2f, 0x100),
|
||||
'Lost Woods Hideout': (0xe1, 0x200),
|
||||
'Lumberjack Tree': (0xe2, 0x200),
|
||||
'Cave 45': (0x11b, 0x400),
|
||||
'Graveyard Cave': (0x11b, 0x200),
|
||||
'Checkerboard Cave': (0x126, 0x200),
|
||||
'Mini Moldorm Cave - Far Left': (0x123, 0x10),
|
||||
'Mini Moldorm Cave - Left': (0x123, 0x20),
|
||||
'Mini Moldorm Cave - Right': (0x123, 0x40),
|
||||
'Mini Moldorm Cave - Far Right': (0x123, 0x80),
|
||||
'Mini Moldorm Cave - Generous Guy': (0x123, 0x400),
|
||||
'Ice Rod Cave': (0x120, 0x10),
|
||||
'Bonk Rock Cave': (0x124, 0x10),
|
||||
'Desert Palace - Big Chest': (0x73, 0x10),
|
||||
'Desert Palace - Torch': (0x73, 0x400),
|
||||
'Desert Palace - Map Chest': (0x74, 0x10),
|
||||
'Desert Palace - Compass Chest': (0x85, 0x10),
|
||||
'Desert Palace - Big Key Chest': (0x75, 0x10),
|
||||
'Desert Palace - Desert Tiles 1 Pot Key': (0x63, 0x400),
|
||||
'Desert Palace - Beamos Hall Pot Key': (0x53, 0x400),
|
||||
'Desert Palace - Desert Tiles 2 Pot Key': (0x43, 0x400),
|
||||
'Desert Palace - Boss': (0x33, 0x800),
|
||||
'Eastern Palace - Compass Chest': (0xa8, 0x10),
|
||||
'Eastern Palace - Big Chest': (0xa9, 0x10),
|
||||
'Eastern Palace - Dark Square Pot Key': (0xba, 0x400),
|
||||
'Eastern Palace - Dark Eyegore Key Drop': (0x99, 0x400),
|
||||
'Eastern Palace - Cannonball Chest': (0xb9, 0x10),
|
||||
'Eastern Palace - Big Key Chest': (0xb8, 0x10),
|
||||
'Eastern Palace - Map Chest': (0xaa, 0x10),
|
||||
'Eastern Palace - Boss': (0xc8, 0x800),
|
||||
'Hyrule Castle - Boomerang Chest': (0x71, 0x10),
|
||||
'Hyrule Castle - Boomerang Guard Key Drop': (0x71, 0x400),
|
||||
'Hyrule Castle - Map Chest': (0x72, 0x10),
|
||||
'Hyrule Castle - Map Guard Key Drop': (0x72, 0x400),
|
||||
"Hyrule Castle - Zelda's Chest": (0x80, 0x10),
|
||||
'Hyrule Castle - Big Key Drop': (0x80, 0x400),
|
||||
'Sewers - Dark Cross': (0x32, 0x10),
|
||||
'Hyrule Castle - Key Rat Key Drop': (0x21, 0x400),
|
||||
'Sewers - Secret Room - Left': (0x11, 0x10),
|
||||
'Sewers - Secret Room - Middle': (0x11, 0x20),
|
||||
'Sewers - Secret Room - Right': (0x11, 0x40),
|
||||
'Sanctuary': (0x12, 0x10),
|
||||
'Castle Tower - Room 03': (0xe0, 0x10),
|
||||
'Castle Tower - Dark Maze': (0xd0, 0x10),
|
||||
'Castle Tower - Dark Archer Key Drop': (0xc0, 0x400),
|
||||
'Castle Tower - Circle of Pots Key Drop': (0xb0, 0x400),
|
||||
'Spectacle Rock Cave': (0xea, 0x400),
|
||||
'Paradox Cave Lower - Far Left': (0xef, 0x10),
|
||||
'Paradox Cave Lower - Left': (0xef, 0x20),
|
||||
'Paradox Cave Lower - Right': (0xef, 0x40),
|
||||
'Paradox Cave Lower - Far Right': (0xef, 0x80),
|
||||
'Paradox Cave Lower - Middle': (0xef, 0x100),
|
||||
'Paradox Cave Upper - Left': (0xff, 0x10),
|
||||
'Paradox Cave Upper - Right': (0xff, 0x20),
|
||||
'Spiral Cave': (0xfe, 0x10),
|
||||
'Tower of Hera - Basement Cage': (0x87, 0x400),
|
||||
'Tower of Hera - Map Chest': (0x77, 0x10),
|
||||
'Tower of Hera - Big Key Chest': (0x87, 0x10),
|
||||
'Tower of Hera - Compass Chest': (0x27, 0x20),
|
||||
'Tower of Hera - Big Chest': (0x27, 0x10),
|
||||
'Tower of Hera - Boss': (0x7, 0x800),
|
||||
'Hype Cave - Top': (0x11e, 0x10),
|
||||
'Hype Cave - Middle Right': (0x11e, 0x20),
|
||||
'Hype Cave - Middle Left': (0x11e, 0x40),
|
||||
'Hype Cave - Bottom': (0x11e, 0x80),
|
||||
'Hype Cave - Generous Guy': (0x11e, 0x400),
|
||||
'Peg Cave': (0x127, 0x400),
|
||||
'Pyramid Fairy - Left': (0x116, 0x10),
|
||||
'Pyramid Fairy - Right': (0x116, 0x20),
|
||||
'Brewery': (0x106, 0x10),
|
||||
'C-Shaped House': (0x11c, 0x10),
|
||||
'Chest Game': (0x106, 0x400),
|
||||
'Mire Shed - Left': (0x10d, 0x10),
|
||||
'Mire Shed - Right': (0x10d, 0x20),
|
||||
'Superbunny Cave - Top': (0xf8, 0x10),
|
||||
'Superbunny Cave - Bottom': (0xf8, 0x20),
|
||||
'Spike Cave': (0x117, 0x10),
|
||||
'Hookshot Cave - Top Right': (0x3c, 0x10),
|
||||
'Hookshot Cave - Top Left': (0x3c, 0x20),
|
||||
'Hookshot Cave - Bottom Right': (0x3c, 0x80),
|
||||
'Hookshot Cave - Bottom Left': (0x3c, 0x40),
|
||||
'Mimic Cave': (0x10c, 0x10),
|
||||
'Swamp Palace - Entrance': (0x28, 0x10),
|
||||
'Swamp Palace - Map Chest': (0x37, 0x10),
|
||||
'Swamp Palace - Pot Row Pot Key': (0x38, 0x400),
|
||||
'Swamp Palace - Trench 1 Pot Key': (0x37, 0x400),
|
||||
'Swamp Palace - Hookshot Pot Key': (0x36, 0x400),
|
||||
'Swamp Palace - Big Chest': (0x36, 0x10),
|
||||
'Swamp Palace - Compass Chest': (0x46, 0x10),
|
||||
'Swamp Palace - Trench 2 Pot Key': (0x35, 0x400),
|
||||
'Swamp Palace - Big Key Chest': (0x35, 0x10),
|
||||
'Swamp Palace - West Chest': (0x34, 0x10),
|
||||
'Swamp Palace - Flooded Room - Left': (0x76, 0x10),
|
||||
'Swamp Palace - Flooded Room - Right': (0x76, 0x20),
|
||||
'Swamp Palace - Waterfall Room': (0x66, 0x10),
|
||||
'Swamp Palace - Waterway Pot Key': (0x16, 0x400),
|
||||
'Swamp Palace - Boss': (0x6, 0x800),
|
||||
"Thieves' Town - Big Key Chest": (0xdb, 0x20),
|
||||
"Thieves' Town - Map Chest": (0xdb, 0x10),
|
||||
"Thieves' Town - Compass Chest": (0xdc, 0x10),
|
||||
"Thieves' Town - Ambush Chest": (0xcb, 0x10),
|
||||
"Thieves' Town - Hallway Pot Key": (0xbc, 0x400),
|
||||
"Thieves' Town - Spike Switch Pot Key": (0xab, 0x400),
|
||||
"Thieves' Town - Attic": (0x65, 0x10),
|
||||
"Thieves' Town - Big Chest": (0x44, 0x10),
|
||||
"Thieves' Town - Blind's Cell": (0x45, 0x10),
|
||||
"Thieves' Town - Boss": (0xac, 0x800),
|
||||
'Skull Woods - Compass Chest': (0x67, 0x10),
|
||||
'Skull Woods - Map Chest': (0x58, 0x20),
|
||||
'Skull Woods - Big Chest': (0x58, 0x10),
|
||||
'Skull Woods - Pot Prison': (0x57, 0x20),
|
||||
'Skull Woods - Pinball Room': (0x68, 0x10),
|
||||
'Skull Woods - Big Key Chest': (0x57, 0x10),
|
||||
'Skull Woods - West Lobby Pot Key': (0x56, 0x400),
|
||||
'Skull Woods - Bridge Room': (0x59, 0x10),
|
||||
'Skull Woods - Spike Corner Key Drop': (0x39, 0x400),
|
||||
'Skull Woods - Boss': (0x29, 0x800),
|
||||
'Ice Palace - Jelly Key Drop': (0x0e, 0x400),
|
||||
'Ice Palace - Compass Chest': (0x2e, 0x10),
|
||||
'Ice Palace - Conveyor Key Drop': (0x3e, 0x400),
|
||||
'Ice Palace - Freezor Chest': (0x7e, 0x10),
|
||||
'Ice Palace - Big Chest': (0x9e, 0x10),
|
||||
'Ice Palace - Iced T Room': (0xae, 0x10),
|
||||
'Ice Palace - Many Pots Pot Key': (0x9f, 0x400),
|
||||
'Ice Palace - Spike Room': (0x5f, 0x10),
|
||||
'Ice Palace - Big Key Chest': (0x1f, 0x10),
|
||||
'Ice Palace - Hammer Block Key Drop': (0x3f, 0x400),
|
||||
'Ice Palace - Map Chest': (0x3f, 0x10),
|
||||
'Ice Palace - Boss': (0xde, 0x800),
|
||||
'Misery Mire - Big Chest': (0xc3, 0x10),
|
||||
'Misery Mire - Map Chest': (0xc3, 0x20),
|
||||
'Misery Mire - Main Lobby': (0xc2, 0x10),
|
||||
'Misery Mire - Bridge Chest': (0xa2, 0x10),
|
||||
'Misery Mire - Spikes Pot Key': (0xb3, 0x400),
|
||||
'Misery Mire - Spike Chest': (0xb3, 0x10),
|
||||
'Misery Mire - Fishbone Pot Key': (0xa1, 0x400),
|
||||
'Misery Mire - Conveyor Crystal Key Drop': (0xc1, 0x400),
|
||||
'Misery Mire - Compass Chest': (0xc1, 0x10),
|
||||
'Misery Mire - Big Key Chest': (0xd1, 0x10),
|
||||
'Misery Mire - Boss': (0x90, 0x800),
|
||||
'Turtle Rock - Compass Chest': (0xd6, 0x10),
|
||||
'Turtle Rock - Roller Room - Left': (0xb7, 0x10),
|
||||
'Turtle Rock - Roller Room - Right': (0xb7, 0x20),
|
||||
'Turtle Rock - Pokey 1 Key Drop': (0xb6, 0x400),
|
||||
'Turtle Rock - Chain Chomps': (0xb6, 0x10),
|
||||
'Turtle Rock - Pokey 2 Key Drop': (0x13, 0x400),
|
||||
'Turtle Rock - Big Key Chest': (0x14, 0x10),
|
||||
'Turtle Rock - Big Chest': (0x24, 0x10),
|
||||
'Turtle Rock - Crystaroller Room': (0x4, 0x10),
|
||||
'Turtle Rock - Eye Bridge - Bottom Left': (0xd5, 0x80),
|
||||
'Turtle Rock - Eye Bridge - Bottom Right': (0xd5, 0x40),
|
||||
'Turtle Rock - Eye Bridge - Top Left': (0xd5, 0x20),
|
||||
'Turtle Rock - Eye Bridge - Top Right': (0xd5, 0x10),
|
||||
'Turtle Rock - Boss': (0xa4, 0x800),
|
||||
'Palace of Darkness - Shooter Room': (0x9, 0x10),
|
||||
'Palace of Darkness - The Arena - Bridge': (0x2a, 0x20),
|
||||
'Palace of Darkness - Stalfos Basement': (0xa, 0x10),
|
||||
'Palace of Darkness - Big Key Chest': (0x3a, 0x10),
|
||||
'Palace of Darkness - The Arena - Ledge': (0x2a, 0x10),
|
||||
'Palace of Darkness - Map Chest': (0x2b, 0x10),
|
||||
'Palace of Darkness - Compass Chest': (0x1a, 0x20),
|
||||
'Palace of Darkness - Dark Basement - Left': (0x6a, 0x10),
|
||||
'Palace of Darkness - Dark Basement - Right': (0x6a, 0x20),
|
||||
'Palace of Darkness - Dark Maze - Top': (0x19, 0x10),
|
||||
'Palace of Darkness - Dark Maze - Bottom': (0x19, 0x20),
|
||||
'Palace of Darkness - Big Chest': (0x1a, 0x10),
|
||||
'Palace of Darkness - Harmless Hellway': (0x1a, 0x40),
|
||||
'Palace of Darkness - Boss': (0x5a, 0x800),
|
||||
'Ganons Tower - Conveyor Cross Pot Key': (0x8b, 0x400),
|
||||
"Ganons Tower - Bob's Torch": (0x8c, 0x400),
|
||||
'Ganons Tower - Hope Room - Left': (0x8c, 0x20),
|
||||
'Ganons Tower - Hope Room - Right': (0x8c, 0x40),
|
||||
'Ganons Tower - Tile Room': (0x8d, 0x10),
|
||||
'Ganons Tower - Compass Room - Top Left': (0x9d, 0x10),
|
||||
'Ganons Tower - Compass Room - Top Right': (0x9d, 0x20),
|
||||
'Ganons Tower - Compass Room - Bottom Left': (0x9d, 0x40),
|
||||
'Ganons Tower - Compass Room - Bottom Right': (0x9d, 0x80),
|
||||
'Ganons Tower - Conveyor Star Pits Pot Key': (0x7b, 0x400),
|
||||
'Ganons Tower - DMs Room - Top Left': (0x7b, 0x10),
|
||||
'Ganons Tower - DMs Room - Top Right': (0x7b, 0x20),
|
||||
'Ganons Tower - DMs Room - Bottom Left': (0x7b, 0x40),
|
||||
'Ganons Tower - DMs Room - Bottom Right': (0x7b, 0x80),
|
||||
'Ganons Tower - Map Chest': (0x8b, 0x10),
|
||||
'Ganons Tower - Double Switch Pot Key': (0x9b, 0x400),
|
||||
'Ganons Tower - Firesnake Room': (0x7d, 0x10),
|
||||
'Ganons Tower - Randomizer Room - Top Left': (0x7c, 0x10),
|
||||
'Ganons Tower - Randomizer Room - Top Right': (0x7c, 0x20),
|
||||
'Ganons Tower - Randomizer Room - Bottom Left': (0x7c, 0x40),
|
||||
'Ganons Tower - Randomizer Room - Bottom Right': (0x7c, 0x80),
|
||||
"Ganons Tower - Bob's Chest": (0x8c, 0x80),
|
||||
'Ganons Tower - Big Chest': (0x8c, 0x10),
|
||||
'Ganons Tower - Big Key Room - Left': (0x1c, 0x20),
|
||||
'Ganons Tower - Big Key Room - Right': (0x1c, 0x40),
|
||||
'Ganons Tower - Big Key Chest': (0x1c, 0x10),
|
||||
'Ganons Tower - Mini Helmasaur Room - Left': (0x3d, 0x10),
|
||||
'Ganons Tower - Mini Helmasaur Room - Right': (0x3d, 0x20),
|
||||
'Ganons Tower - Mini Helmasaur Key Drop': (0x3d, 0x400),
|
||||
'Ganons Tower - Pre-Moldorm Chest': (0x3d, 0x40),
|
||||
'Ganons Tower - Validation Chest': (0x4d, 0x10)}
|
||||
|
||||
boss_locations = {Regions.lookup_name_to_id[name] for name in {'Eastern Palace - Boss',
|
||||
'Desert Palace - Boss',
|
||||
'Tower of Hera - Boss',
|
||||
'Palace of Darkness - Boss',
|
||||
'Swamp Palace - Boss',
|
||||
'Skull Woods - Boss',
|
||||
"Thieves' Town - Boss",
|
||||
'Ice Palace - Boss',
|
||||
'Misery Mire - Boss',
|
||||
'Turtle Rock - Boss',
|
||||
'Sahasrahla'}}
|
||||
|
||||
location_table_uw_id = {Regions.lookup_name_to_id[name]: data for name, data in location_table_uw.items()}
|
||||
|
||||
location_table_npc = {'Mushroom': 0x1000,
|
||||
'King Zora': 0x2,
|
||||
'Sahasrahla': 0x10,
|
||||
'Blacksmith': 0x400,
|
||||
'Magic Bat': 0x8000,
|
||||
'Sick Kid': 0x4,
|
||||
'Library': 0x80,
|
||||
'Potion Shop': 0x2000,
|
||||
'Old Man': 0x1,
|
||||
'Ether Tablet': 0x100,
|
||||
'Catfish': 0x20,
|
||||
'Stumpy': 0x8,
|
||||
'Bombos Tablet': 0x200}
|
||||
|
||||
location_table_npc_id = {Regions.lookup_name_to_id[name]: data for name, data in location_table_npc.items()}
|
||||
|
||||
location_table_ow = {'Flute Spot': 0x2a,
|
||||
'Sunken Treasure': 0x3b,
|
||||
"Zora's Ledge": 0x81,
|
||||
'Lake Hylia Island': 0x35,
|
||||
'Maze Race': 0x28,
|
||||
'Desert Ledge': 0x30,
|
||||
'Master Sword Pedestal': 0x80,
|
||||
'Spectacle Rock': 0x3,
|
||||
'Pyramid': 0x5b,
|
||||
'Digging Game': 0x68,
|
||||
'Bumper Cave Ledge': 0x4a,
|
||||
'Floating Island': 0x5}
|
||||
|
||||
location_table_ow_id = {Regions.lookup_name_to_id[name]: data for name, data in location_table_ow.items()}
|
||||
|
||||
location_table_misc = {'Bottle Merchant': (0x3c9, 0x2),
|
||||
'Purple Chest': (0x3c9, 0x10),
|
||||
"Link's Uncle": (0x3c6, 0x1),
|
||||
'Hobo': (0x3c9, 0x1)}
|
||||
location_table_misc_id = {Regions.lookup_name_to_id[name]: data for name, data in location_table_misc.items()}
|
||||
|
||||
|
||||
async def track_locations(ctx, roomid, roomdata):
|
||||
from SNIClient import snes_read, snes_buffered_write, snes_flush_writes
|
||||
new_locations = []
|
||||
|
||||
def new_check(location_id):
|
||||
new_locations.append(location_id)
|
||||
ctx.locations_checked.add(location_id)
|
||||
location = ctx.location_names[location_id]
|
||||
snes_logger.info(
|
||||
f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})')
|
||||
|
||||
try:
|
||||
shop_data = await snes_read(ctx, SHOP_ADDR, SHOP_LEN)
|
||||
shop_data_changed = False
|
||||
shop_data = list(shop_data)
|
||||
for cnt, b in enumerate(shop_data):
|
||||
location = Shops.SHOP_ID_START + cnt
|
||||
if int(b) and location not in ctx.locations_checked:
|
||||
new_check(location)
|
||||
if ctx.allow_collect and location in ctx.checked_locations and location not in ctx.locations_checked \
|
||||
and location in ctx.locations_info and ctx.locations_info[location].player != ctx.slot:
|
||||
if not int(b):
|
||||
shop_data[cnt] += 1
|
||||
shop_data_changed = True
|
||||
if shop_data_changed:
|
||||
snes_buffered_write(ctx, SHOP_ADDR, bytes(shop_data))
|
||||
except Exception as e:
|
||||
snes_logger.info(f"Exception: {e}")
|
||||
|
||||
for location_id, (loc_roomid, loc_mask) in location_table_uw_id.items():
|
||||
try:
|
||||
if location_id not in ctx.locations_checked and loc_roomid == roomid and \
|
||||
(roomdata << 4) & loc_mask != 0:
|
||||
new_check(location_id)
|
||||
except Exception as e:
|
||||
snes_logger.exception(f"Exception: {e}")
|
||||
|
||||
uw_begin = 0x129
|
||||
ow_end = uw_end = 0
|
||||
uw_unchecked = {}
|
||||
uw_checked = {}
|
||||
for location, (roomid, mask) in location_table_uw.items():
|
||||
location_id = Regions.lookup_name_to_id[location]
|
||||
if location_id not in ctx.locations_checked:
|
||||
uw_unchecked[location_id] = (roomid, mask)
|
||||
uw_begin = min(uw_begin, roomid)
|
||||
uw_end = max(uw_end, roomid + 1)
|
||||
if ctx.allow_collect and location_id not in boss_locations and location_id in ctx.checked_locations \
|
||||
and location_id not in ctx.locations_checked and location_id in ctx.locations_info \
|
||||
and ctx.locations_info[location_id].player != ctx.slot:
|
||||
uw_begin = min(uw_begin, roomid)
|
||||
uw_end = max(uw_end, roomid + 1)
|
||||
uw_checked[location_id] = (roomid, mask)
|
||||
|
||||
if uw_begin < uw_end:
|
||||
uw_data = await snes_read(ctx, SAVEDATA_START + (uw_begin * 2), (uw_end - uw_begin) * 2)
|
||||
if uw_data is not None:
|
||||
for location_id, (roomid, mask) in uw_unchecked.items():
|
||||
offset = (roomid - uw_begin) * 2
|
||||
roomdata = uw_data[offset] | (uw_data[offset + 1] << 8)
|
||||
if roomdata & mask != 0:
|
||||
new_check(location_id)
|
||||
if uw_checked:
|
||||
uw_data = list(uw_data)
|
||||
for location_id, (roomid, mask) in uw_checked.items():
|
||||
offset = (roomid - uw_begin) * 2
|
||||
roomdata = uw_data[offset] | (uw_data[offset + 1] << 8)
|
||||
roomdata |= mask
|
||||
uw_data[offset] = roomdata & 0xFF
|
||||
uw_data[offset + 1] = roomdata >> 8
|
||||
snes_buffered_write(ctx, SAVEDATA_START + (uw_begin * 2), bytes(uw_data))
|
||||
|
||||
ow_begin = 0x82
|
||||
ow_unchecked = {}
|
||||
ow_checked = {}
|
||||
for location_id, screenid in location_table_ow_id.items():
|
||||
if location_id not in ctx.locations_checked:
|
||||
ow_unchecked[location_id] = screenid
|
||||
ow_begin = min(ow_begin, screenid)
|
||||
ow_end = max(ow_end, screenid + 1)
|
||||
if ctx.allow_collect and location_id in ctx.checked_locations and location_id in ctx.locations_info \
|
||||
and ctx.locations_info[location_id].player != ctx.slot:
|
||||
ow_checked[location_id] = screenid
|
||||
|
||||
if ow_begin < ow_end:
|
||||
ow_data = await snes_read(ctx, SAVEDATA_START + 0x280 + ow_begin, ow_end - ow_begin)
|
||||
if ow_data is not None:
|
||||
for location_id, screenid in ow_unchecked.items():
|
||||
if ow_data[screenid - ow_begin] & 0x40 != 0:
|
||||
new_check(location_id)
|
||||
if ow_checked:
|
||||
ow_data = list(ow_data)
|
||||
for location_id, screenid in ow_checked.items():
|
||||
ow_data[screenid - ow_begin] |= 0x40
|
||||
snes_buffered_write(ctx, SAVEDATA_START + 0x280 + ow_begin, bytes(ow_data))
|
||||
|
||||
if not ctx.locations_checked.issuperset(location_table_npc_id):
|
||||
npc_data = await snes_read(ctx, SAVEDATA_START + 0x410, 2)
|
||||
if npc_data is not None:
|
||||
npc_value_changed = False
|
||||
npc_value = npc_data[0] | (npc_data[1] << 8)
|
||||
for location_id, mask in location_table_npc_id.items():
|
||||
if npc_value & mask != 0 and location_id not in ctx.locations_checked:
|
||||
new_check(location_id)
|
||||
if ctx.allow_collect and location_id not in boss_locations and location_id in ctx.checked_locations \
|
||||
and location_id not in ctx.locations_checked and location_id in ctx.locations_info \
|
||||
and ctx.locations_info[location_id].player != ctx.slot:
|
||||
npc_value |= mask
|
||||
npc_value_changed = True
|
||||
if npc_value_changed:
|
||||
npc_data = bytes([npc_value & 0xFF, npc_value >> 8])
|
||||
snes_buffered_write(ctx, SAVEDATA_START + 0x410, npc_data)
|
||||
|
||||
if not ctx.locations_checked.issuperset(location_table_misc_id):
|
||||
misc_data = await snes_read(ctx, SAVEDATA_START + 0x3c6, 4)
|
||||
if misc_data is not None:
|
||||
misc_data = list(misc_data)
|
||||
misc_data_changed = False
|
||||
for location_id, (offset, mask) in location_table_misc_id.items():
|
||||
assert (0x3c6 <= offset <= 0x3c9)
|
||||
if misc_data[offset - 0x3c6] & mask != 0 and location_id not in ctx.locations_checked:
|
||||
new_check(location_id)
|
||||
if ctx.allow_collect and location_id in ctx.checked_locations and location_id not in ctx.locations_checked \
|
||||
and location_id in ctx.locations_info and ctx.locations_info[location_id].player != ctx.slot:
|
||||
misc_data_changed = True
|
||||
misc_data[offset - 0x3c6] |= mask
|
||||
if misc_data_changed:
|
||||
snes_buffered_write(ctx, SAVEDATA_START + 0x3c6, bytes(misc_data))
|
||||
|
||||
|
||||
if new_locations:
|
||||
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": new_locations}])
|
||||
await snes_flush_writes(ctx)
|
||||
|
||||
|
||||
def get_alttp_settings(romfile: str):
|
||||
lastSettings = Utils.get_adjuster_settings(GAME_ALTTP)
|
||||
adjustedromfile = ''
|
||||
if lastSettings:
|
||||
choice = 'no'
|
||||
if not hasattr(lastSettings, 'auto_apply') or 'ask' in lastSettings.auto_apply:
|
||||
|
||||
whitelist = {"music", "menuspeed", "heartbeep", "heartcolor", "ow_palettes", "quickswap",
|
||||
"uw_palettes", "sprite", "sword_palettes", "shield_palettes", "hud_palettes",
|
||||
"reduceflashing", "deathlink", "allowcollect"}
|
||||
printed_options = {name: value for name, value in vars(lastSettings).items() if name in whitelist}
|
||||
if hasattr(lastSettings, "sprite_pool"):
|
||||
sprite_pool = {}
|
||||
for sprite in lastSettings.sprite_pool:
|
||||
if sprite in sprite_pool:
|
||||
sprite_pool[sprite] += 1
|
||||
else:
|
||||
sprite_pool[sprite] = 1
|
||||
if sprite_pool:
|
||||
printed_options["sprite_pool"] = sprite_pool
|
||||
import pprint
|
||||
|
||||
from CommonClient import gui_enabled
|
||||
if gui_enabled:
|
||||
|
||||
try:
|
||||
from tkinter import Tk, PhotoImage, Label, LabelFrame, Frame, Button
|
||||
applyPromptWindow = Tk()
|
||||
except Exception as e:
|
||||
logging.error('Could not load tkinter, which is likely not installed.')
|
||||
return '', False
|
||||
|
||||
applyPromptWindow.resizable(False, False)
|
||||
applyPromptWindow.protocol('WM_DELETE_WINDOW', lambda: onButtonClick())
|
||||
logo = PhotoImage(file=Utils.local_path('data', 'icon.png'))
|
||||
applyPromptWindow.tk.call('wm', 'iconphoto', applyPromptWindow._w, logo)
|
||||
applyPromptWindow.wm_title("Last adjuster settings LttP")
|
||||
|
||||
label = LabelFrame(applyPromptWindow,
|
||||
text='Last used adjuster settings were found. Would you like to apply these?')
|
||||
label.grid(column=0, row=0, padx=5, pady=5, ipadx=5, ipady=5)
|
||||
label.grid_columnconfigure(0, weight=1)
|
||||
label.grid_columnconfigure(1, weight=1)
|
||||
label.grid_columnconfigure(2, weight=1)
|
||||
label.grid_columnconfigure(3, weight=1)
|
||||
|
||||
def onButtonClick(answer: str = 'no'):
|
||||
setattr(onButtonClick, 'choice', answer)
|
||||
applyPromptWindow.destroy()
|
||||
|
||||
framedOptions = Frame(label)
|
||||
framedOptions.grid(column=0, columnspan=4, row=0)
|
||||
framedOptions.grid_columnconfigure(0, weight=1)
|
||||
framedOptions.grid_columnconfigure(1, weight=1)
|
||||
framedOptions.grid_columnconfigure(2, weight=1)
|
||||
curRow = 0
|
||||
curCol = 0
|
||||
for name, value in printed_options.items():
|
||||
Label(framedOptions, text=name + ": " + str(value)).grid(column=curCol, row=curRow, padx=5)
|
||||
if (curCol == 2):
|
||||
curRow += 1
|
||||
curCol = 0
|
||||
else:
|
||||
curCol += 1
|
||||
|
||||
yesButton = Button(label, text='Yes', command=lambda: onButtonClick('yes'), width=10)
|
||||
yesButton.grid(column=0, row=1)
|
||||
noButton = Button(label, text='No', command=lambda: onButtonClick('no'), width=10)
|
||||
noButton.grid(column=1, row=1)
|
||||
alwaysButton = Button(label, text='Always', command=lambda: onButtonClick('always'), width=10)
|
||||
alwaysButton.grid(column=2, row=1)
|
||||
neverButton = Button(label, text='Never', command=lambda: onButtonClick('never'), width=10)
|
||||
neverButton.grid(column=3, row=1)
|
||||
|
||||
Utils.tkinter_center_window(applyPromptWindow)
|
||||
applyPromptWindow.mainloop()
|
||||
choice = getattr(onButtonClick, 'choice')
|
||||
else:
|
||||
choice = input(f"Last used adjuster settings were found. Would you like to apply these? \n"
|
||||
f"{pprint.pformat(printed_options)}\n"
|
||||
f"Enter yes, no, always or never: ")
|
||||
if choice and choice.startswith("y"):
|
||||
choice = 'yes'
|
||||
elif choice and "never" in choice:
|
||||
choice = 'no'
|
||||
lastSettings.auto_apply = 'never'
|
||||
Utils.persistent_store("adjuster", GAME_ALTTP, lastSettings)
|
||||
elif choice and "always" in choice:
|
||||
choice = 'yes'
|
||||
lastSettings.auto_apply = 'always'
|
||||
Utils.persistent_store("adjuster", GAME_ALTTP, lastSettings)
|
||||
else:
|
||||
choice = 'no'
|
||||
elif 'never' in lastSettings.auto_apply:
|
||||
choice = 'no'
|
||||
elif 'always' in lastSettings.auto_apply:
|
||||
choice = 'yes'
|
||||
|
||||
if 'yes' in choice:
|
||||
from worlds.alttp.Rom import get_base_rom_path
|
||||
lastSettings.rom = romfile
|
||||
lastSettings.baserom = get_base_rom_path()
|
||||
lastSettings.world = None
|
||||
|
||||
if hasattr(lastSettings, "sprite_pool"):
|
||||
from LttPAdjuster import AdjusterWorld
|
||||
lastSettings.world = AdjusterWorld(getattr(lastSettings, "sprite_pool"))
|
||||
|
||||
adjusted = True
|
||||
import LttPAdjuster
|
||||
_, adjustedromfile = LttPAdjuster.adjust(lastSettings)
|
||||
|
||||
if hasattr(lastSettings, "world"):
|
||||
delattr(lastSettings, "world")
|
||||
else:
|
||||
adjusted = False
|
||||
if adjusted:
|
||||
try:
|
||||
shutil.move(adjustedromfile, romfile)
|
||||
adjustedromfile = romfile
|
||||
except Exception as e:
|
||||
logging.exception(e)
|
||||
else:
|
||||
adjusted = False
|
||||
return adjustedromfile, adjusted
|
||||
|
||||
|
||||
class ALTTPSNIClient(SNIClient):
|
||||
game = "A Link to the Past"
|
||||
|
||||
async def deathlink_kill_player(self, ctx):
|
||||
from SNIClient import DeathState, snes_read, snes_buffered_write, snes_flush_writes
|
||||
invincible = await snes_read(ctx, WRAM_START + 0x037B, 1)
|
||||
last_health = await snes_read(ctx, WRAM_START + 0xF36D, 1)
|
||||
await asyncio.sleep(0.25)
|
||||
health = await snes_read(ctx, WRAM_START + 0xF36D, 1)
|
||||
if not invincible or not last_health or not health:
|
||||
ctx.death_state = DeathState.dead
|
||||
ctx.last_death_link = time.time()
|
||||
return
|
||||
if not invincible[0] and last_health[0] == health[0]:
|
||||
snes_buffered_write(ctx, WRAM_START + 0xF36D, bytes([0])) # set current health to 0
|
||||
snes_buffered_write(ctx, WRAM_START + 0x0373,
|
||||
bytes([8])) # deal 1 full heart of damage at next opportunity
|
||||
|
||||
await snes_flush_writes(ctx)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
gamemode = await snes_read(ctx, WRAM_START + 0x10, 1)
|
||||
if not gamemode or gamemode[0] in DEATH_MODES:
|
||||
ctx.death_state = DeathState.dead
|
||||
|
||||
|
||||
async def validate_rom(self, ctx):
|
||||
from SNIClient import snes_read, snes_buffered_write, snes_flush_writes
|
||||
|
||||
rom_name = await snes_read(ctx, ROMNAME_START, ROMNAME_SIZE)
|
||||
if rom_name is None or rom_name == bytes([0] * ROMNAME_SIZE) or rom_name[:2] != b"AP":
|
||||
return False
|
||||
|
||||
ctx.game = self.game
|
||||
ctx.items_handling = 0b001 # full local
|
||||
|
||||
ctx.rom = rom_name
|
||||
|
||||
death_link = await snes_read(ctx, DEATH_LINK_ACTIVE_ADDR, 1)
|
||||
|
||||
if death_link:
|
||||
ctx.allow_collect = bool(death_link[0] & 0b100)
|
||||
ctx.death_link_allow_survive = bool(death_link[0] & 0b10)
|
||||
await ctx.update_death_link(bool(death_link[0] & 0b1))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def game_watcher(self, ctx):
|
||||
from SNIClient import snes_read, snes_buffered_write, snes_flush_writes
|
||||
gamemode = await snes_read(ctx, WRAM_START + 0x10, 1)
|
||||
if "DeathLink" in ctx.tags and gamemode and ctx.last_death_link + 1 < time.time():
|
||||
currently_dead = gamemode[0] in DEATH_MODES
|
||||
await ctx.handle_deathlink_state(currently_dead)
|
||||
|
||||
gameend = await snes_read(ctx, SAVEDATA_START + 0x443, 1)
|
||||
game_timer = await snes_read(ctx, SAVEDATA_START + 0x42E, 4)
|
||||
if gamemode is None or gameend is None or game_timer is None or \
|
||||
(gamemode[0] not in INGAME_MODES and gamemode[0] not in ENDGAME_MODES):
|
||||
return
|
||||
|
||||
if gameend[0]:
|
||||
if not ctx.finished_game:
|
||||
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
||||
ctx.finished_game = True
|
||||
|
||||
if gamemode in ENDGAME_MODES: # triforce room and credits
|
||||
return
|
||||
|
||||
data = await snes_read(ctx, RECV_PROGRESS_ADDR, 8)
|
||||
if data is None:
|
||||
return
|
||||
|
||||
recv_index = data[0] | (data[1] << 8)
|
||||
recv_item = data[2]
|
||||
roomid = data[4] | (data[5] << 8)
|
||||
roomdata = data[6]
|
||||
scout_location = data[7]
|
||||
|
||||
if recv_index < len(ctx.items_received) and recv_item == 0:
|
||||
item = ctx.items_received[recv_index]
|
||||
recv_index += 1
|
||||
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
|
||||
color(ctx.item_names[item.item], 'red', 'bold'),
|
||||
color(ctx.player_names[item.player], 'yellow'),
|
||||
ctx.location_names[item.location], recv_index, len(ctx.items_received)))
|
||||
|
||||
snes_buffered_write(ctx, RECV_PROGRESS_ADDR,
|
||||
bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
|
||||
snes_buffered_write(ctx, RECV_ITEM_ADDR,
|
||||
bytes([item.item]))
|
||||
snes_buffered_write(ctx, RECV_ITEM_PLAYER_ADDR,
|
||||
bytes([min(ROM_PLAYER_LIMIT, item.player) if item.player != ctx.slot else 0]))
|
||||
if scout_location > 0 and scout_location in ctx.locations_info:
|
||||
snes_buffered_write(ctx, SCOUTREPLY_LOCATION_ADDR,
|
||||
bytes([scout_location]))
|
||||
snes_buffered_write(ctx, SCOUTREPLY_ITEM_ADDR,
|
||||
bytes([ctx.locations_info[scout_location].item]))
|
||||
snes_buffered_write(ctx, SCOUTREPLY_PLAYER_ADDR,
|
||||
bytes([min(ROM_PLAYER_LIMIT, ctx.locations_info[scout_location].player)]))
|
||||
|
||||
await snes_flush_writes(ctx)
|
||||
|
||||
if scout_location > 0 and scout_location not in ctx.locations_scouted:
|
||||
ctx.locations_scouted.add(scout_location)
|
||||
await ctx.send_msgs([{"cmd": "LocationScouts", "locations": [scout_location]}])
|
||||
await track_locations(ctx, roomid, roomdata)
|
||||
@@ -3834,14 +3834,21 @@ inverted_default_dungeon_connections = [('Desert Palace Entrance (South)', 'Dese
|
||||
|
||||
# Regions that can be required to access entrances through rules, not paths
|
||||
indirect_connections = {
|
||||
'Turtle Rock (Top)': 'Turtle Rock',
|
||||
'East Dark World': 'Pyramid Fairy',
|
||||
'Big Bomb Shop': 'Pyramid Fairy',
|
||||
'Dark Desert': 'Pyramid Fairy',
|
||||
'West Dark World': 'Pyramid Fairy',
|
||||
'South Dark World': 'Pyramid Fairy',
|
||||
'Light World': 'Pyramid Fairy',
|
||||
'Old Man Cave': 'Old Man S&Q'
|
||||
"Turtle Rock (Top)": "Turtle Rock",
|
||||
"East Dark World": "Pyramid Fairy",
|
||||
"Dark Desert": "Pyramid Fairy",
|
||||
"West Dark World": "Pyramid Fairy",
|
||||
"South Dark World": "Pyramid Fairy",
|
||||
"Light World": "Pyramid Fairy",
|
||||
"Old Man Cave": "Old Man S&Q"
|
||||
}
|
||||
|
||||
indirect_connections_inverted = {
|
||||
"Inverted Big Bomb Shop": "Pyramid Fairy",
|
||||
}
|
||||
|
||||
indirect_connections_not_inverted = {
|
||||
"Big Bomb Shop": "Pyramid Fairy",
|
||||
}
|
||||
|
||||
# format:
|
||||
|
||||
+16
-97
@@ -1,7 +1,7 @@
|
||||
import typing
|
||||
|
||||
from BaseClasses import MultiWorld
|
||||
from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, TextChoice
|
||||
from Options import Choice, Range, Option, Toggle, DefaultOnToggle, DeathLink, TextChoice, PlandoBosses
|
||||
|
||||
|
||||
class Logic(Choice):
|
||||
@@ -138,7 +138,7 @@ class WorldState(Choice):
|
||||
option_inverted = 2
|
||||
|
||||
|
||||
class Bosses(TextChoice):
|
||||
class LTTPBosses(PlandoBosses):
|
||||
"""Shuffles bosses around to different locations.
|
||||
Basic will shuffle all bosses except Ganon and Agahnim anywhere they can be placed.
|
||||
Full chooses 3 bosses at random to be placed twice instead of Lanmolas, Moldorm, and Helmasaur.
|
||||
@@ -152,7 +152,9 @@ class Bosses(TextChoice):
|
||||
option_chaos = 3
|
||||
option_singularity = 4
|
||||
|
||||
bosses: set = {
|
||||
duplicate_bosses = True
|
||||
|
||||
bosses = {
|
||||
"Armos Knights",
|
||||
"Lanmolas",
|
||||
"Moldorm",
|
||||
@@ -165,7 +167,7 @@ class Bosses(TextChoice):
|
||||
"Trinexx",
|
||||
}
|
||||
|
||||
locations: set = {
|
||||
locations = {
|
||||
"Ganons Tower Top",
|
||||
"Tower of Hera",
|
||||
"Skull Woods",
|
||||
@@ -181,99 +183,16 @@ class Bosses(TextChoice):
|
||||
"Ganons Tower Bottom"
|
||||
}
|
||||
|
||||
def __init__(self, value: typing.Union[str, int]):
|
||||
assert isinstance(value, str) or isinstance(value, int), \
|
||||
f"{value} is not a valid option for {self.__class__.__name__}"
|
||||
self.value = value
|
||||
|
||||
@classmethod
|
||||
def from_text(cls, text: str):
|
||||
import random
|
||||
# set all of our text to lower case for name checking
|
||||
text = text.lower()
|
||||
cls.bosses = {boss_name.lower() for boss_name in cls.bosses}
|
||||
cls.locations = {boss_location.lower() for boss_location in cls.locations}
|
||||
if text == "random":
|
||||
return cls(random.choice(list(cls.options.values())))
|
||||
for option_name, value in cls.options.items():
|
||||
if option_name == text:
|
||||
return cls(value)
|
||||
options = text.split(";")
|
||||
|
||||
# since plando exists in the option verify the plando values given are valid
|
||||
cls.validate_plando_bosses(options)
|
||||
|
||||
# find out what type of boss shuffle we should use for placing bosses after plando
|
||||
# and add as a string to look nice in the spoiler
|
||||
if "random" in options:
|
||||
shuffle = random.choice(list(cls.options))
|
||||
options.remove("random")
|
||||
options = ";".join(options) + ";" + shuffle
|
||||
boss_class = cls(options)
|
||||
else:
|
||||
for option in options:
|
||||
if option in cls.options:
|
||||
boss_class = cls(";".join(options))
|
||||
break
|
||||
else:
|
||||
if len(options) == 1:
|
||||
if cls.valid_boss_name(options[0]):
|
||||
options = options[0] + ";singularity"
|
||||
boss_class = cls(options)
|
||||
else:
|
||||
options = options[0] + ";none"
|
||||
boss_class = cls(options)
|
||||
else:
|
||||
options = ";".join(options) + ";none"
|
||||
boss_class = cls(options)
|
||||
return boss_class
|
||||
|
||||
@classmethod
|
||||
def validate_plando_bosses(cls, options: typing.List[str]) -> None:
|
||||
from .Bosses import can_place_boss, format_boss_location
|
||||
for option in options:
|
||||
if option == "random" or option in cls.options:
|
||||
if option != options[-1]:
|
||||
raise ValueError(f"{option} option must be at the end of the boss_shuffle options!")
|
||||
continue
|
||||
if "-" in option:
|
||||
location, boss = option.split("-")
|
||||
level = ''
|
||||
if not cls.valid_boss_name(boss):
|
||||
raise ValueError(f"{boss} is not a valid boss name for location {location}.")
|
||||
if not cls.valid_location_name(location):
|
||||
raise ValueError(f"{location} is not a valid boss location name.")
|
||||
if location.split(" ")[-1] in ("top", "middle", "bottom"):
|
||||
location = location.split(" ")
|
||||
level = location[-1]
|
||||
location = " ".join(location[:-1])
|
||||
location = location.title().replace("Of", "of")
|
||||
if not can_place_boss(boss.title(), location, level):
|
||||
raise ValueError(f"{format_boss_location(location, level)} "
|
||||
f"is not a valid location for {boss.title()}.")
|
||||
else:
|
||||
if not cls.valid_boss_name(option):
|
||||
raise ValueError(f"{option} is not a valid boss name.")
|
||||
|
||||
@classmethod
|
||||
def valid_boss_name(cls, value: str) -> bool:
|
||||
return value.lower() in cls.bosses
|
||||
|
||||
@classmethod
|
||||
def valid_location_name(cls, value: str) -> bool:
|
||||
return value in cls.locations
|
||||
|
||||
def verify(self, world, player_name: str, plando_options) -> None:
|
||||
if isinstance(self.value, int):
|
||||
return
|
||||
from Generate import PlandoSettings
|
||||
if not(PlandoSettings.bosses & plando_options):
|
||||
import logging
|
||||
# plando is disabled but plando options were given so pull the option and change it to an int
|
||||
option = self.value.split(";")[-1]
|
||||
self.value = self.options[option]
|
||||
logging.warning(f"The plando bosses module is turned off, so {self.name_lookup[self.value].title()} "
|
||||
f"boss shuffle will be used for player {player_name}.")
|
||||
def can_place_boss(cls, boss: str, location: str) -> bool:
|
||||
from worlds.alttp.Bosses import can_place_boss
|
||||
level = ''
|
||||
words = location.split(" ")
|
||||
if words[-1] in ("top", "middle", "bottom"):
|
||||
level = words[-1]
|
||||
location = " ".join(words[:-1])
|
||||
location = location.title().replace("Of", "of")
|
||||
return can_place_boss(boss.title(), location, level)
|
||||
|
||||
|
||||
class Enemies(Choice):
|
||||
@@ -497,7 +416,7 @@ alttp_options: typing.Dict[str, type(Option)] = {
|
||||
"hints": Hints,
|
||||
"scams": Scams,
|
||||
"restrict_dungeon_item_on_boss": RestrictBossItem,
|
||||
"boss_shuffle": Bosses,
|
||||
"boss_shuffle": LTTPBosses,
|
||||
"pot_shuffle": PotShuffle,
|
||||
"enemy_shuffle": EnemyShuffle,
|
||||
"killable_thieves": KillableThieves,
|
||||
|
||||
@@ -4,6 +4,10 @@ import typing
|
||||
from BaseClasses import Region, Entrance, RegionType
|
||||
|
||||
|
||||
def is_main_entrance(entrance: Entrance) -> bool:
|
||||
return entrance.parent_region.type in {RegionType.DarkWorld, RegionType.LightWorld, RegionType.Generic}
|
||||
|
||||
|
||||
def create_regions(world, player):
|
||||
|
||||
world.regions += [
|
||||
|
||||
+22
-26
@@ -1,11 +1,12 @@
|
||||
from __future__ import annotations
|
||||
|
||||
import Utils
|
||||
from Patch import read_rom
|
||||
import worlds.AutoWorld
|
||||
import worlds.Files
|
||||
|
||||
LTTPJPN10HASH = '03a63945398191337e896e5771f77173'
|
||||
RANDOMIZERBASEHASH = '9952c2a3ec1b421e408df0d20c8f0c7f'
|
||||
ROM_PLAYER_LIMIT = 255
|
||||
LTTPJPN10HASH: str = "03a63945398191337e896e5771f77173"
|
||||
RANDOMIZERBASEHASH: str = "9952c2a3ec1b421e408df0d20c8f0c7f"
|
||||
ROM_PLAYER_LIMIT: int = 255
|
||||
|
||||
import io
|
||||
import json
|
||||
@@ -34,7 +35,7 @@ from worlds.alttp.Text import KingsReturn_texts, Sanctuary_texts, Kakariko_texts
|
||||
DeathMountain_texts, \
|
||||
LostWoods_texts, WishingWell_texts, DesertPalace_texts, MountainTower_texts, LinksHouse_texts, Lumberjacks_texts, \
|
||||
SickKid_texts, FluteBoy_texts, Zora_texts, MagicShop_texts, Sahasrahla_names
|
||||
from Utils import local_path, user_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen, parse_yaml
|
||||
from Utils import local_path, user_path, int16_as_bytes, int32_as_bytes, snes_to_pc, is_frozen, parse_yaml, read_snes_rom
|
||||
from worlds.alttp.Items import ItemFactory, item_table, item_name_groups, progression_items
|
||||
from worlds.alttp.EntranceShuffle import door_addresses
|
||||
from worlds.alttp.Options import smallkey_shuffle
|
||||
@@ -57,13 +58,13 @@ class LocalRom(object):
|
||||
self.orig_buffer = None
|
||||
|
||||
with open(file, 'rb') as stream:
|
||||
self.buffer = read_rom(stream)
|
||||
self.buffer = read_snes_rom(stream)
|
||||
if patch:
|
||||
self.patch_base_rom()
|
||||
self.orig_buffer = self.buffer.copy()
|
||||
if vanillaRom:
|
||||
with open(vanillaRom, 'rb') as vanillaStream:
|
||||
self.orig_buffer = read_rom(vanillaStream)
|
||||
self.orig_buffer = read_snes_rom(vanillaStream)
|
||||
|
||||
def read_byte(self, address: int) -> int:
|
||||
return self.buffer[address]
|
||||
@@ -123,29 +124,24 @@ class LocalRom(object):
|
||||
return expected == buffermd5.hexdigest()
|
||||
|
||||
def patch_base_rom(self):
|
||||
if os.path.isfile(local_path('basepatch.sfc')):
|
||||
with open(local_path('basepatch.sfc'), 'rb') as stream:
|
||||
if os.path.isfile(user_path('basepatch.sfc')):
|
||||
with open(user_path('basepatch.sfc'), 'rb') as stream:
|
||||
buffer = bytearray(stream.read())
|
||||
|
||||
if self.verify(buffer):
|
||||
self.buffer = buffer
|
||||
if not os.path.exists(local_path('data', 'basepatch.apbp')):
|
||||
Patch.create_patch_file(local_path('basepatch.sfc'))
|
||||
return
|
||||
|
||||
if not os.path.isfile(local_path('data', 'basepatch.apbp')):
|
||||
raise RuntimeError('Base patch unverified. Unable to continue.')
|
||||
with open(local_path("data", "basepatch.bsdiff4"), "rb") as f:
|
||||
delta = f.read()
|
||||
|
||||
if os.path.isfile(local_path('data', 'basepatch.apbp')):
|
||||
_, target, buffer = Patch.create_rom_bytes(local_path('data', 'basepatch.apbp'), ignore_version=True)
|
||||
if self.verify(buffer):
|
||||
self.buffer = bytearray(buffer)
|
||||
with open(user_path('basepatch.sfc'), 'wb') as stream:
|
||||
stream.write(buffer)
|
||||
return
|
||||
raise RuntimeError('Base patch unverified. Unable to continue.')
|
||||
|
||||
raise RuntimeError('Could not find Base Patch. Unable to continue.')
|
||||
buffer = bsdiff4.patch(get_base_rom_bytes(), delta)
|
||||
if self.verify(buffer):
|
||||
self.buffer = bytearray(buffer)
|
||||
with open(user_path('basepatch.sfc'), 'wb') as stream:
|
||||
stream.write(buffer)
|
||||
return
|
||||
raise RuntimeError('Base patch unverified. Unable to continue.')
|
||||
|
||||
def write_crc(self):
|
||||
crc = (sum(self.buffer[:0x7FDC] + self.buffer[0x7FE0:]) + 0x01FE) & 0xFFFF
|
||||
@@ -544,7 +540,7 @@ class Sprite():
|
||||
|
||||
def get_vanilla_sprite_data(self):
|
||||
file_name = get_base_rom_path()
|
||||
base_rom_bytes = bytes(read_rom(open(file_name, "rb")))
|
||||
base_rom_bytes = bytes(read_snes_rom(open(file_name, "rb")))
|
||||
Sprite.sprite = base_rom_bytes[0x80000:0x87000]
|
||||
Sprite.palette = base_rom_bytes[0xDD308:0xDD380]
|
||||
Sprite.glove_palette = base_rom_bytes[0xDEDF5:0xDEDF9]
|
||||
@@ -2906,7 +2902,7 @@ hash_alphabet = [
|
||||
]
|
||||
|
||||
|
||||
class LttPDeltaPatch(Patch.APDeltaPatch):
|
||||
class LttPDeltaPatch(worlds.Files.APDeltaPatch):
|
||||
hash = LTTPJPN10HASH
|
||||
game = "A Link to the Past"
|
||||
patch_file_ending = ".aplttp"
|
||||
@@ -2920,7 +2916,7 @@ def get_base_rom_bytes(file_name: str = "") -> bytes:
|
||||
base_rom_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 = bytes(read_rom(open(file_name, "rb")))
|
||||
base_rom_bytes = bytes(read_snes_rom(open(file_name, "rb")))
|
||||
|
||||
basemd5 = hashlib.md5()
|
||||
basemd5.update(base_rom_bytes)
|
||||
|
||||
+42
-13
@@ -7,23 +7,27 @@ import typing
|
||||
import Utils
|
||||
from BaseClasses import Item, CollectionState, Tutorial
|
||||
from .Dungeons import create_dungeons
|
||||
from .EntranceShuffle import link_entrances, link_inverted_entrances, plando_connect
|
||||
from .EntranceShuffle import link_entrances, link_inverted_entrances, plando_connect, \
|
||||
indirect_connections, indirect_connections_inverted, indirect_connections_not_inverted
|
||||
from .InvertedRegions import create_inverted_regions, mark_dark_world_regions
|
||||
from .ItemPool import generate_itempool, difficulties
|
||||
from .Items import item_init_table, item_name_groups, item_table, GetBeemizerItem
|
||||
from .Options import alttp_options, smallkey_shuffle
|
||||
from .Regions import lookup_name_to_id, create_regions, mark_light_world_regions
|
||||
from .Regions import lookup_name_to_id, create_regions, mark_light_world_regions, lookup_vanilla_location_to_entrance, \
|
||||
is_main_entrance
|
||||
from .Client import ALTTPSNIClient
|
||||
from .Rom import LocalRom, patch_rom, patch_race_rom, check_enemizer, patch_enemizer, apply_rom_settings, \
|
||||
get_hash_string, get_base_rom_path, LttPDeltaPatch
|
||||
from .Rules import set_rules
|
||||
from .Shops import create_shops, ShopSlotFill
|
||||
from .SubClasses import ALttPItem
|
||||
from ..AutoWorld import World, WebWorld, LogicMixin
|
||||
from worlds.AutoWorld import World, WebWorld, LogicMixin
|
||||
|
||||
lttp_logger = logging.getLogger("A Link to the Past")
|
||||
|
||||
extras_list = sum(difficulties['normal'].extras[0:5], [])
|
||||
|
||||
|
||||
class ALTTPWeb(WebWorld):
|
||||
setup_en = Tutorial(
|
||||
"Multiworld Setup Tutorial",
|
||||
@@ -214,13 +218,24 @@ class ALTTPWorld(World):
|
||||
if world.mode[player] != 'inverted':
|
||||
link_entrances(world, player)
|
||||
mark_light_world_regions(world, player)
|
||||
for region_name, entrance_name in indirect_connections_not_inverted.items():
|
||||
world.register_indirect_condition(self.world.get_region(region_name, player),
|
||||
self.world.get_entrance(entrance_name, player))
|
||||
else:
|
||||
link_inverted_entrances(world, player)
|
||||
mark_dark_world_regions(world, player)
|
||||
for region_name, entrance_name in indirect_connections_inverted.items():
|
||||
world.register_indirect_condition(self.world.get_region(region_name, player),
|
||||
self.world.get_entrance(entrance_name, player))
|
||||
|
||||
world.random = old_random
|
||||
plando_connect(world, player)
|
||||
|
||||
for region_name, entrance_name in indirect_connections.items():
|
||||
world.register_indirect_condition(self.world.get_region(region_name, player),
|
||||
self.world.get_entrance(entrance_name, player))
|
||||
|
||||
|
||||
def collect_item(self, state: CollectionState, item: Item, remove=False):
|
||||
item_name = item.name
|
||||
if item_name.startswith('Progressive '):
|
||||
@@ -394,11 +409,7 @@ class ALTTPWorld(World):
|
||||
deathlink=world.death_link[player],
|
||||
allowcollect=world.allow_collect[player])
|
||||
|
||||
outfilepname = f'_P{player}'
|
||||
outfilepname += f"_{world.get_file_safe_player_name(player).replace(' ', '_')}" \
|
||||
if world.player_name[player] != 'Player%d' % player else ''
|
||||
|
||||
rompath = os.path.join(output_directory, f'AP_{world.seed_name}{outfilepname}.sfc')
|
||||
rompath = os.path.join(output_directory, f"{self.world.get_out_file_name_base(self.player)}.sfc")
|
||||
rom.write_to_file(rompath)
|
||||
patch = LttPDeltaPatch(os.path.splitext(rompath)[0]+LttPDeltaPatch.patch_file_ending, player=player,
|
||||
player_name=world.player_name[player], patched_path=rompath)
|
||||
@@ -410,6 +421,20 @@ class ALTTPWorld(World):
|
||||
finally:
|
||||
self.rom_name_available_event.set() # make sure threading continues and errors are collected
|
||||
|
||||
@classmethod
|
||||
def stage_extend_hint_information(cls, world, hint_data: typing.Dict[int, typing.Dict[int, str]]):
|
||||
er_hint_data = {player: {} for player in world.get_game_players("A Link to the Past") if
|
||||
world.shuffle[player] != "vanilla" or world.retro_caves[player]}
|
||||
|
||||
for region in world.regions:
|
||||
if region.player in er_hint_data and region.locations:
|
||||
main_entrance = region.get_connecting_entrance(is_main_entrance)
|
||||
for location in region.locations:
|
||||
if type(location.address) == int: # skips events and crystals
|
||||
if lookup_vanilla_location_to_entrance[location.address] != main_entrance.name:
|
||||
er_hint_data[region.player][location.address] = main_entrance.name
|
||||
hint_data.update(er_hint_data)
|
||||
|
||||
def modify_multidata(self, multidata: dict):
|
||||
import base64
|
||||
# wait for self.rom_name to be available.
|
||||
@@ -474,11 +499,15 @@ class ALTTPWorld(World):
|
||||
|
||||
while gtower_locations and filleritempool and trash_count > 0:
|
||||
spot_to_fill = gtower_locations.pop()
|
||||
item_to_place = filleritempool.pop()
|
||||
if spot_to_fill.item_rule(item_to_place):
|
||||
world.push_item(spot_to_fill, item_to_place, False)
|
||||
fill_locations.remove(spot_to_fill) # very slow, unfortunately
|
||||
trash_count -= 1
|
||||
for index, item in enumerate(filleritempool):
|
||||
if spot_to_fill.item_rule(item):
|
||||
filleritempool.pop(index) # remove from outer fill
|
||||
world.push_item(spot_to_fill, item, False)
|
||||
fill_locations.remove(spot_to_fill) # very slow, unfortunately
|
||||
trash_count -= 1
|
||||
break
|
||||
else:
|
||||
logging.warning(f"Could not trash fill Ganon's Tower for player {player}.")
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
if self.world.goal[self.player] == "icerodhunt":
|
||||
|
||||
@@ -6,7 +6,7 @@
|
||||
- [SNI](https://github.com/alttpo/sni/releases) (Integriert in Archipelago)
|
||||
- Hardware oder Software zum Laden und Abspielen von SNES Rom-Dateien fähig zu einer Internetverbindung
|
||||
- Ein Emulator, der mit SNI verbinden kann
|
||||
([snes9x Multitroid](https://drive.google.com/drive/folders/1_ej-pwWtCAHYXIrvs5Hro16A1s9Hi3Jz),
|
||||
([snes9x rr](https://github.com/gocha/snes9x-rr/releases),
|
||||
[BizHawk](http://tasvideos.org/BizHawk.html))
|
||||
- Ein SD2SNES, [FXPak Pro](https://krikzz.com/store/home/54-fxpak-pro.html), oder andere kompatible Hardware
|
||||
- Die Japanische Zelda 1.0 ROM-Datei, mit folgendem Namen: `Zelda no Densetsu - Kamigami no Triforce (Japan).sfc`
|
||||
@@ -93,7 +93,7 @@ Wenn der client den Emulator automatisch gestartet hat, wird SNI ebenfalls im Hi
|
||||
Mal ist, wird möglicherweise ein Fenster angezeigt, wo man bestätigen muss, dass das Programm durch die Windows Firewall
|
||||
kommunizieren darf.
|
||||
|
||||
##### snes9x Multitroid
|
||||
##### snes9x-rr
|
||||
|
||||
1. Lade die Entsprechende ROM-Datei, wenn sie nicht schon automatisch geladen wurde.
|
||||
2. Klicke auf den Reiter "File" oben im Menü und wähle **Lua Scripting**
|
||||
|
||||
@@ -75,7 +75,7 @@ client, and will also create your ROM in the same place as your patch file.
|
||||
When the client launched automatically, SNI should have also automatically launched in the background. If this is its
|
||||
first time launching, you may be prompted to allow it to communicate through the Windows Firewall.
|
||||
|
||||
##### snes9x Multitroid
|
||||
##### snes9x-rr
|
||||
|
||||
1. Load your ROM file if it hasn't already been loaded.
|
||||
2. Click on the File menu and hover on **Lua Scripting**
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
- [QUsb2Snes](https://github.com/Skarsnik/QUsb2snes/releases) (Incluido en Multiworld Utilities)
|
||||
- Hardware o software capaz de cargar y ejecutar archivos de ROM de SNES
|
||||
- Un emulador capaz de ejecutar scripts Lua
|
||||
([snes9x Multitroid](https://drive.google.com/drive/folders/1_ej-pwWtCAHYXIrvs5Hro16A1s9Hi3Jz),
|
||||
([snes9x rr](https://github.com/gocha/snes9x-rr/releases),
|
||||
[BizHawk](http://tasvideos.org/BizHawk.html), o
|
||||
[RetroArch](https://retroarch.com?page=platforms) 1.10.1 o más nuevo). O,
|
||||
- Un flashcart SD2SNES, [FXPak Pro](https://krikzz.com/store/home/54-fxpak-pro.html), o otro hardware compatible
|
||||
@@ -111,13 +111,13 @@ automáticamente el cliente, y ademas creara la rom en el mismo directorio donde
|
||||
Cuando el cliente se lance automáticamente, QUsb2Snes debería haberse ejecutado también. Si es la primera vez que lo
|
||||
ejecutas, puedes ser que el firewall de Windows te pregunte si le permites la comunicación.
|
||||
|
||||
##### snes9x Multitroid
|
||||
##### snes9x-rr
|
||||
|
||||
1. Carga tu fichero de ROM, si no lo has hecho ya
|
||||
2. Abre el menu "File" y situa el raton en **Lua Scripting**
|
||||
3. Haz click en **New Lua Script Window...**
|
||||
4. En la nueva ventana, haz click en **Browse...**
|
||||
5. Navega hacia el directorio donde este situado snes9x Multitroid, entra en el directorio `lua`, y
|
||||
5. Navega hacia el directorio donde este situado snes9x-rr, entra en el directorio `lua`, y
|
||||
escoge `multibridge.lua`
|
||||
6. Observa que se ha asignado un nombre al dispositivo, y el cliente muestra "SNES Device: Connected", con el mismo
|
||||
nombre en la esquina superior izquierda.
|
||||
|
||||
@@ -12,7 +12,7 @@
|
||||
- [QUsb2Snes](https://github.com/Skarsnik/QUsb2snes/releases) (Inclus dans les utilitaires précédents)
|
||||
- Une solution logicielle ou matérielle capable de charger et de lancer des fichiers ROM de SNES
|
||||
- Un émulateur capable d'éxécuter des scripts Lua
|
||||
([snes9x Multitroid](https://drive.google.com/drive/folders/1_ej-pwWtCAHYXIrvs5Hro16A1s9Hi3Jz),
|
||||
([snes9x rr](https://github.com/gocha/snes9x-rr/releases),
|
||||
[BizHawk](http://tasvideos.org/BizHawk.html))
|
||||
- Un SD2SNES, [FXPak Pro](https://krikzz.com/store/home/54-fxpak-pro.html), ou une autre solution matérielle
|
||||
compatible
|
||||
@@ -112,13 +112,13 @@ Quand le client se lance automatiquement, QUsb2Snes devrait se lancer automatiqu
|
||||
c'est la première fois qu'il démarre, il vous sera peut-être demandé de l'autoriser à communiquer à travers le pare-feu
|
||||
Windows.
|
||||
|
||||
##### snes9x Multitroid
|
||||
##### snes9x-rr
|
||||
|
||||
1. Chargez votre ROM si ce n'est pas déjà fait.
|
||||
2. Cliquez sur le menu "File" et survolez l'option **Lua Scripting**
|
||||
3. Cliquez alors sur **New Lua Script Window...**
|
||||
4. Dans la nouvelle fenêtre, sélectionnez **Browse...**
|
||||
5. Dirigez vous vers le dossier où vous avez extrait snes9x Multitroid, allez dans le dossier `lua`, puis
|
||||
5. Dirigez vous vers le dossier où vous avez extrait snes9x-rr, allez dans le dossier `lua`, puis
|
||||
choisissez `multibridge.lua`
|
||||
6. Remarquez qu'un nom vous a été assigné, et que l'interface Web affiche "SNES Device: Connected", avec ce même nom
|
||||
dans le coin en haut à gauche.
|
||||
|
||||
@@ -0,0 +1,33 @@
|
||||
from BaseClasses import Tutorial
|
||||
from ..AutoWorld import World, WebWorld
|
||||
from typing import Dict
|
||||
|
||||
|
||||
class Bk_SudokuWebWorld(WebWorld):
|
||||
settings_page = "games/Sudoku/info/en"
|
||||
theme = 'partyTime'
|
||||
tutorials = [
|
||||
Tutorial(
|
||||
tutorial_name='Setup Guide',
|
||||
description='A guide to playing BK Sudoku',
|
||||
language='English',
|
||||
file_name='setup_en.md',
|
||||
link='guide/en',
|
||||
authors=['Jarno']
|
||||
)
|
||||
]
|
||||
|
||||
|
||||
class Bk_SudokuWorld(World):
|
||||
"""
|
||||
Play a little Sudoku while you're in BK mode to maybe get some useful hints
|
||||
"""
|
||||
game = "Sudoku"
|
||||
web = Bk_SudokuWebWorld()
|
||||
|
||||
item_name_to_id: Dict[str, int] = {}
|
||||
location_name_to_id: Dict[str, int] = {}
|
||||
|
||||
@classmethod
|
||||
def stage_assert_generate(cls, world):
|
||||
raise Exception("BK Sudoku cannot be used for generating worlds, the client can instead connect to any other world")
|
||||
@@ -0,0 +1,13 @@
|
||||
# Bk Sudoku
|
||||
|
||||
## What is this game?
|
||||
|
||||
BK Sudoku is not a typical Archipelago game; instead, it is a generic Sudoku client that can connect to any existing multiworld. When connected, you can play Sudoku to unlock random hints for your game. While slow, it will give you something to do when you can't reach the checks in your game.
|
||||
|
||||
## What hints are unlocked?
|
||||
|
||||
After completing a Sudoku puzzle, the game will unlock 1 random hint for an unchecked location in the slot you are connected to. It is possible to hint the same location repeatedly if that location is still unchecked.
|
||||
|
||||
## Where is the settings page?
|
||||
|
||||
There is no settings page; this game cannot be used in your .yamls. Instead, the client can connect to any slot in a multiworld.
|
||||
@@ -0,0 +1,24 @@
|
||||
# BK Sudoku Setup Guide
|
||||
|
||||
## Required Software
|
||||
- [Bk Sudoku](https://github.com/Jarno458/sudoku)
|
||||
- [.Net 6](https://docs.microsoft.com/en-us/dotnet/core/install/windows?tabs=net60)
|
||||
|
||||
## General Concept
|
||||
|
||||
This is a client that can connect to any multiworld slot, and lets you play Sudoku to unlock random hints for that slot's locations.
|
||||
|
||||
Due to the fact that the Sudoku client may connect to any slot, it is not necessary to generate a YAML for this game as it does not generate any new slots in the multiworld session.
|
||||
|
||||
## Installation Procedures
|
||||
|
||||
Go to the latest release on [BK Sudoku Releases](https://github.com/Jarno458/sudoku/releases). Download and extract the `Bk_Sudoku.zip` file.
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
1. Run Bk_Sudoku.exe
|
||||
2. Enter the name of the slot you wish to connect to
|
||||
3. Enter the server url & port number
|
||||
4. Press connect
|
||||
5. Choose difficulty
|
||||
6. Try to solve the Sudoku
|
||||
@@ -0,0 +1,17 @@
|
||||
from BaseClasses import Item
|
||||
from worlds.dark_souls_3.data.items_data import item_tables
|
||||
|
||||
|
||||
class DarkSouls3Item(Item):
|
||||
game: str = "Dark Souls III"
|
||||
|
||||
@staticmethod
|
||||
def get_name_to_id() -> dict:
|
||||
base_id = 100000
|
||||
table_offset = 100
|
||||
|
||||
output = {}
|
||||
for i, table in enumerate(item_tables):
|
||||
output.update({name: id for id, name in enumerate(table, base_id + (table_offset * i))})
|
||||
|
||||
return output
|
||||
@@ -0,0 +1,17 @@
|
||||
from BaseClasses import Location
|
||||
from worlds.dark_souls_3.data.locations_data import location_tables
|
||||
|
||||
|
||||
class DarkSouls3Location(Location):
|
||||
game: str = "Dark Souls III"
|
||||
|
||||
@staticmethod
|
||||
def get_name_to_id() -> dict:
|
||||
base_id = 100000
|
||||
table_offset = 100
|
||||
|
||||
output = {}
|
||||
for i, table in enumerate(location_tables):
|
||||
output.update({name: id for id, name in enumerate(table, base_id + (table_offset * i))})
|
||||
|
||||
return output
|
||||
@@ -3,9 +3,11 @@ import json
|
||||
import os
|
||||
from typing import Dict
|
||||
|
||||
from .Items import DarkSouls3Item
|
||||
from .Locations import DarkSouls3Location
|
||||
from .Options import dark_souls_options
|
||||
from .data.items_data import weapons_upgrade_5_table, weapons_upgrade_10_table, item_dictionary_table, key_items_list
|
||||
from .data.locations_data import location_dictionary_table, cemetery_of_ash_table, fire_link_shrine_table, \
|
||||
from .data.items_data import weapons_upgrade_5_table, weapons_upgrade_10_table, item_dictionary, key_items_list
|
||||
from .data.locations_data import location_dictionary, fire_link_shrine_table, \
|
||||
high_wall_of_lothric, \
|
||||
undead_settlement_table, road_of_sacrifice_table, consumed_king_garden_table, cathedral_of_the_deep_table, \
|
||||
farron_keep_table, catacombs_of_carthus_table, smouldering_lake_table, irithyll_of_the_boreal_valley_table, \
|
||||
@@ -52,10 +54,11 @@ class DarkSouls3World(World):
|
||||
remote_items: bool = False
|
||||
remote_start_inventory: bool = False
|
||||
web = DarkSouls3Web()
|
||||
data_version = 2
|
||||
data_version = 3
|
||||
base_id = 100000
|
||||
item_name_to_id = {name: id for id, name in enumerate(item_dictionary_table, base_id)}
|
||||
location_name_to_id = {name: id for id, name in enumerate(location_dictionary_table, base_id)}
|
||||
required_client_version = (0, 3, 6)
|
||||
item_name_to_id = DarkSouls3Item.get_name_to_id()
|
||||
location_name_to_id = DarkSouls3Location.get_name_to_id()
|
||||
|
||||
def __init__(self, world: MultiWorld, player: int):
|
||||
super().__init__(world, player)
|
||||
@@ -80,7 +83,6 @@ class DarkSouls3World(World):
|
||||
self.world.regions.append(menu_region)
|
||||
|
||||
# Create all Vanilla regions of Dark Souls III
|
||||
cemetery_of_ash_region = self.create_region("Cemetery Of Ash", cemetery_of_ash_table)
|
||||
firelink_shrine_region = self.create_region("Firelink Shrine", fire_link_shrine_table)
|
||||
firelink_shrine_bell_tower_region = self.create_region("Firelink Shrine Bell Tower",
|
||||
firelink_shrine_bell_tower_table)
|
||||
@@ -105,9 +107,7 @@ class DarkSouls3World(World):
|
||||
|
||||
# Create the entrance to connect those regions
|
||||
menu_region.exits.append(Entrance(self.player, "New Game", menu_region))
|
||||
self.world.get_entrance("New Game", self.player).connect(cemetery_of_ash_region)
|
||||
cemetery_of_ash_region.exits.append(Entrance(self.player, "Goto Firelink Shrine", cemetery_of_ash_region))
|
||||
self.world.get_entrance("Goto Firelink Shrine", self.player).connect(firelink_shrine_region)
|
||||
self.world.get_entrance("New Game", self.player).connect(firelink_shrine_region)
|
||||
firelink_shrine_region.exits.append(Entrance(self.player, "Goto High Wall of Lothric",
|
||||
firelink_shrine_region))
|
||||
firelink_shrine_region.exits.append(Entrance(self.player, "Goto Kiln Of The First Flame",
|
||||
@@ -211,7 +211,7 @@ class DarkSouls3World(World):
|
||||
set_rule(self.world.get_location("ID: Covetous Gold Serpent Ring", self.player),
|
||||
lambda state: state.has("Old Cell Key", self.player))
|
||||
set_rule(self.world.get_location("ID: Karla's Ashes", self.player),
|
||||
lambda state: state.has("Jailers Key Ring", self.player))
|
||||
lambda state: state.has("Jailer's Key Ring", self.player))
|
||||
black_hand_gotthard_corpse_rule = lambda state: \
|
||||
(state.can_reach("AL: Cinders of a Lord - Aldrich", "Location", self.player) and
|
||||
state.can_reach("PC: Cinders of a Lord - Yhorm the Giant", "Location", self.player))
|
||||
@@ -242,18 +242,18 @@ class DarkSouls3World(World):
|
||||
slot_data: Dict[str, object] = {}
|
||||
|
||||
# Depending on the specified option, modify items hexadecimal value to add an upgrade level
|
||||
item_dictionary = item_dictionary_table.copy()
|
||||
item_dictionary_copy = item_dictionary.copy()
|
||||
if self.world.randomize_weapons_level[self.player]:
|
||||
# Randomize some weapons upgrades
|
||||
for name in weapons_upgrade_5_table.keys():
|
||||
if self.world.random.randint(0, 100) < 33:
|
||||
value = self.world.random.randint(1, 5)
|
||||
item_dictionary[name] += value
|
||||
item_dictionary_copy[name] += value
|
||||
|
||||
for name in weapons_upgrade_10_table.keys():
|
||||
if self.world.random.randint(0, 100) < 33:
|
||||
value = self.world.random.randint(1, 10)
|
||||
item_dictionary[name] += value
|
||||
item_dictionary_copy[name] += value
|
||||
|
||||
# Create the mandatory lists to generate the player's output file
|
||||
items_id = []
|
||||
@@ -267,7 +267,7 @@ class DarkSouls3World(World):
|
||||
items_address.append(item_dictionary[location.item.name])
|
||||
|
||||
if location.player == self.player:
|
||||
locations_address.append(location_dictionary_table[location.name])
|
||||
locations_address.append(location_dictionary[location.name])
|
||||
locations_id.append(location.address)
|
||||
if location.item.player == self.player:
|
||||
locations_target.append(item_dictionary[location.item.name])
|
||||
@@ -294,11 +294,3 @@ class DarkSouls3World(World):
|
||||
|
||||
def generate_output(self, output_directory: str):
|
||||
pass
|
||||
|
||||
|
||||
class DarkSouls3Location(Location):
|
||||
game: str = "Dark Souls III"
|
||||
|
||||
|
||||
class DarkSouls3Item(Item):
|
||||
game: str = "Dark Souls III"
|
||||
|
||||
@@ -391,4 +391,6 @@ key_items_list = {
|
||||
"Jailer's Key Ring",
|
||||
}
|
||||
|
||||
item_dictionary_table = {**weapons_upgrade_5_table, **weapons_upgrade_10_table, **shields_table, **armor_table, **rings_table, **spells_table, **misc_items_table, **goods_table}
|
||||
item_tables = [weapons_upgrade_5_table, weapons_upgrade_10_table, shields_table, armor_table, rings_table, spells_table, misc_items_table, goods_table]
|
||||
|
||||
item_dictionary = {**weapons_upgrade_5_table, **weapons_upgrade_10_table, **shields_table, **armor_table, **rings_table, **spells_table, **misc_items_table, **goods_table}
|
||||
|
||||
@@ -5,9 +5,6 @@ Regular expression parser https://regex101.com/r/XdtiLR/2
|
||||
List of locations https://darksouls3.wiki.fextralife.com/Locations
|
||||
"""
|
||||
|
||||
cemetery_of_ash_table = {
|
||||
}
|
||||
|
||||
fire_link_shrine_table = {
|
||||
# "FS: Coiled Sword": 0x40000859, You can still light the Firelink Shrine fire whether you have it or not, useless
|
||||
"FS: Broken Straight Sword": 0x001EF9B0,
|
||||
@@ -440,7 +437,12 @@ archdragon_peak_table = {
|
||||
"AP: Havel's Greatshield": 0x013376F0,
|
||||
}
|
||||
|
||||
location_dictionary_table = {**cemetery_of_ash_table, **fire_link_shrine_table, **firelink_shrine_bell_tower_table, **high_wall_of_lothric, **undead_settlement_table, **road_of_sacrifice_table,
|
||||
**cathedral_of_the_deep_table, **farron_keep_table, **catacombs_of_carthus_table, **smouldering_lake_table, **irithyll_of_the_boreal_valley_table,
|
||||
**irithyll_dungeon_table, **profaned_capital_table, **anor_londo_table, **lothric_castle_table, **consumed_king_garden_table,
|
||||
**grand_archives_table, **untended_graves_table, **archdragon_peak_table}
|
||||
location_tables = [fire_link_shrine_table, firelink_shrine_bell_tower_table, high_wall_of_lothric, undead_settlement_table, road_of_sacrifice_table,
|
||||
cathedral_of_the_deep_table, farron_keep_table, catacombs_of_carthus_table, smouldering_lake_table, irithyll_of_the_boreal_valley_table,
|
||||
irithyll_dungeon_table, profaned_capital_table, anor_londo_table, lothric_castle_table, consumed_king_garden_table,
|
||||
grand_archives_table, untended_graves_table, archdragon_peak_table]
|
||||
|
||||
location_dictionary = {**fire_link_shrine_table, **firelink_shrine_bell_tower_table, **high_wall_of_lothric, **undead_settlement_table, **road_of_sacrifice_table,
|
||||
**cathedral_of_the_deep_table, **farron_keep_table, **catacombs_of_carthus_table, **smouldering_lake_table, **irithyll_of_the_boreal_valley_table,
|
||||
**irithyll_dungeon_table, **profaned_capital_table, **anor_londo_table, **lothric_castle_table, **consumed_king_garden_table,
|
||||
**grand_archives_table, **untended_graves_table, **archdragon_peak_table}
|
||||
|
||||
+27
-37
@@ -2,75 +2,69 @@ import logging
|
||||
import asyncio
|
||||
|
||||
from NetUtils import ClientStatus, color
|
||||
from SNIClient import Context, snes_buffered_write, snes_flush_writes, snes_read
|
||||
from Patch import GAME_DKC3
|
||||
from worlds.AutoSNIClient import SNIClient
|
||||
|
||||
snes_logger = logging.getLogger("SNES")
|
||||
|
||||
# DKC3 - DKC3_TODO: Check these values
|
||||
# FXPAK Pro protocol memory mapping used by SNI
|
||||
ROM_START = 0x000000
|
||||
WRAM_START = 0xF50000
|
||||
WRAM_SIZE = 0x20000
|
||||
SRAM_START = 0xE00000
|
||||
|
||||
SAVEDATA_START = WRAM_START + 0xF000
|
||||
SAVEDATA_SIZE = 0x500
|
||||
|
||||
DKC3_ROMNAME_START = 0x00FFC0
|
||||
DKC3_ROMHASH_START = 0x7FC0
|
||||
ROMNAME_SIZE = 0x15
|
||||
ROMHASH_SIZE = 0x15
|
||||
|
||||
DKC3_RECV_PROGRESS_ADDR = WRAM_START + 0x632 # DKC3_TODO: Find a permanent home for this
|
||||
DKC3_RECV_PROGRESS_ADDR = WRAM_START + 0x632
|
||||
DKC3_FILE_NAME_ADDR = WRAM_START + 0x5D9
|
||||
DEATH_LINK_ACTIVE_ADDR = DKC3_ROMNAME_START + 0x15 # DKC3_TODO: Find a permanent home for this
|
||||
|
||||
|
||||
async def deathlink_kill_player(ctx: Context):
|
||||
pass
|
||||
#if ctx.game == GAME_DKC3:
|
||||
class DKC3SNIClient(SNIClient):
|
||||
game = "Donkey Kong Country 3"
|
||||
|
||||
async def deathlink_kill_player(self, ctx):
|
||||
pass
|
||||
# DKC3_TODO: Handle Receiving Deathlink
|
||||
|
||||
|
||||
async def dkc3_rom_init(ctx: Context):
|
||||
if not ctx.rom:
|
||||
ctx.finished_game = False
|
||||
ctx.death_link_allow_survive = False
|
||||
game_name = await snes_read(ctx, DKC3_ROMNAME_START, 0x15)
|
||||
if game_name is None or game_name != b"DONKEY KONG COUNTRY 3":
|
||||
return False
|
||||
else:
|
||||
ctx.game = GAME_DKC3
|
||||
ctx.items_handling = 0b111 # remote items
|
||||
async def validate_rom(self, ctx):
|
||||
from SNIClient import snes_buffered_write, snes_flush_writes, snes_read
|
||||
|
||||
rom = await snes_read(ctx, DKC3_ROMHASH_START, ROMHASH_SIZE)
|
||||
if rom is None or rom == bytes([0] * ROMHASH_SIZE):
|
||||
rom_name = await snes_read(ctx, DKC3_ROMHASH_START, ROMHASH_SIZE)
|
||||
if rom_name is None or rom_name == bytes([0] * ROMHASH_SIZE) or rom_name[:2] != b"D3":
|
||||
return False
|
||||
|
||||
ctx.rom = rom
|
||||
ctx.game = self.game
|
||||
ctx.items_handling = 0b111 # remote items
|
||||
|
||||
ctx.rom = rom_name
|
||||
|
||||
#death_link = await snes_read(ctx, DEATH_LINK_ACTIVE_ADDR, 1)
|
||||
## DKC3_TODO: Handle Deathlink
|
||||
#if death_link:
|
||||
# ctx.allow_collect = bool(death_link[0] & 0b100)
|
||||
# await ctx.update_death_link(bool(death_link[0] & 0b1))
|
||||
return True
|
||||
return True
|
||||
|
||||
|
||||
async def dkc3_game_watcher(ctx: Context):
|
||||
if ctx.game == GAME_DKC3:
|
||||
async def game_watcher(self, ctx):
|
||||
from SNIClient import snes_buffered_write, snes_flush_writes, snes_read
|
||||
# DKC3_TODO: Handle Deathlink
|
||||
save_file_name = await snes_read(ctx, DKC3_FILE_NAME_ADDR, 0x5)
|
||||
if save_file_name is None or save_file_name[0] == 0x00:
|
||||
if save_file_name is None or save_file_name[0] == 0x00 or save_file_name == bytes([0x55] * 0x05):
|
||||
# We haven't loaded a save file
|
||||
return
|
||||
|
||||
new_checks = []
|
||||
from worlds.dkc3.Rom import location_rom_data, item_rom_data, boss_location_ids, level_unlock_map
|
||||
location_ram_data = await snes_read(ctx, WRAM_START + 0x5FE, 0x81)
|
||||
for loc_id, loc_data in location_rom_data.items():
|
||||
if loc_id not in ctx.locations_checked:
|
||||
data = await snes_read(ctx, WRAM_START + loc_data[0], 1)
|
||||
masked_data = data[0] & (1 << loc_data[1])
|
||||
data = location_ram_data[loc_data[0] - 0x5FE]
|
||||
masked_data = data & (1 << loc_data[1])
|
||||
bit_set = (masked_data != 0)
|
||||
invert_bit = ((len(loc_data) >= 3) and loc_data[2])
|
||||
if bit_set != invert_bit:
|
||||
@@ -78,8 +72,9 @@ async def dkc3_game_watcher(ctx: Context):
|
||||
new_checks.append(loc_id)
|
||||
|
||||
verify_save_file_name = await snes_read(ctx, DKC3_FILE_NAME_ADDR, 0x5)
|
||||
if verify_save_file_name is None or verify_save_file_name[0] == 0x00 or verify_save_file_name != save_file_name:
|
||||
if verify_save_file_name is None or verify_save_file_name[0] == 0x00 or verify_save_file_name == bytes([0x55] * 0x05) or verify_save_file_name != save_file_name:
|
||||
# We have somehow exited the save file (or worse)
|
||||
ctx.rom = None
|
||||
return
|
||||
|
||||
rom = await snes_read(ctx, DKC3_ROMHASH_START, ROMHASH_SIZE)
|
||||
@@ -184,8 +179,9 @@ async def dkc3_game_watcher(ctx: Context):
|
||||
|
||||
await snes_flush_writes(ctx)
|
||||
|
||||
# DKC3_TODO: This method of collect should work, however it does not unlock the next level correctly when previous is flagged
|
||||
# Handle Collected Locations
|
||||
levels_to_tiles = await snes_read(ctx, ROM_START + 0x3FF800, 0x60)
|
||||
tiles_to_levels = await snes_read(ctx, ROM_START + 0x3FF860, 0x60)
|
||||
for loc_id in ctx.checked_locations:
|
||||
if loc_id not in ctx.locations_checked and loc_id not in boss_location_ids:
|
||||
loc_data = location_rom_data[loc_id]
|
||||
@@ -193,30 +189,24 @@ async def dkc3_game_watcher(ctx: Context):
|
||||
invert_bit = ((len(loc_data) >= 3) and loc_data[2])
|
||||
if not invert_bit:
|
||||
masked_data = data[0] | (1 << loc_data[1])
|
||||
#print("Collected Location: ", hex(loc_data[0]), " | ", loc_data[1])
|
||||
snes_buffered_write(ctx, WRAM_START + loc_data[0], bytes([masked_data]))
|
||||
|
||||
if (loc_data[1] == 1):
|
||||
# Make the next levels accessible
|
||||
level_id = loc_data[0] - 0x632
|
||||
levels_to_tiles = await snes_read(ctx, ROM_START + 0x3FF800, 0x60)
|
||||
tiles_to_levels = await snes_read(ctx, ROM_START + 0x3FF860, 0x60)
|
||||
tile_id = levels_to_tiles[level_id] if levels_to_tiles[level_id] != 0xFF else level_id
|
||||
tile_id = tile_id + 0x632
|
||||
#print("Tile ID: ", hex(tile_id))
|
||||
if tile_id in level_unlock_map:
|
||||
for next_level_address in level_unlock_map[tile_id]:
|
||||
next_level_id = next_level_address - 0x632
|
||||
next_tile_id = tiles_to_levels[next_level_id] if tiles_to_levels[next_level_id] != 0xFF else next_level_id
|
||||
next_tile_id = next_tile_id + 0x632
|
||||
#print("Next Level ID: ", hex(next_tile_id))
|
||||
next_data = await snes_read(ctx, WRAM_START + next_tile_id, 1)
|
||||
snes_buffered_write(ctx, WRAM_START + next_tile_id, bytes([next_data[0] | 0x01]))
|
||||
|
||||
await snes_flush_writes(ctx)
|
||||
else:
|
||||
masked_data = data[0] & ~(1 << loc_data[1])
|
||||
print("Collected Inverted Location: ", hex(loc_data[0]), " | ", loc_data[1])
|
||||
snes_buffered_write(ctx, WRAM_START + loc_data[0], bytes([masked_data]))
|
||||
await snes_flush_writes(ctx)
|
||||
ctx.locations_checked.add(loc_id)
|
||||
|
||||
+5
-4
@@ -1,5 +1,6 @@
|
||||
import Utils
|
||||
from Patch import read_rom, APDeltaPatch
|
||||
from Utils import read_snes_rom
|
||||
from worlds.Files import APDeltaPatch
|
||||
from .Locations import lookup_id_to_name, all_locations
|
||||
from .Levels import level_list, level_dict
|
||||
|
||||
@@ -440,13 +441,13 @@ class LocalRom(object):
|
||||
self.orig_buffer = None
|
||||
|
||||
with open(file, 'rb') as stream:
|
||||
self.buffer = read_rom(stream)
|
||||
self.buffer = read_snes_rom(stream)
|
||||
#if patch:
|
||||
# self.patch_rom()
|
||||
# self.orig_buffer = self.buffer.copy()
|
||||
#if vanillaRom:
|
||||
# with open(vanillaRom, 'rb') as vanillaStream:
|
||||
# self.orig_buffer = read_rom(vanillaStream)
|
||||
# self.orig_buffer = read_snes_rom(vanillaStream)
|
||||
|
||||
def read_bit(self, address: int, bit_number: int) -> bool:
|
||||
bitflag = (1 << bit_number)
|
||||
@@ -724,7 +725,7 @@ def get_base_rom_bytes(file_name: str = "") -> bytes:
|
||||
base_rom_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 = bytes(read_rom(open(file_name, "rb")))
|
||||
base_rom_bytes = bytes(read_snes_rom(open(file_name, "rb")))
|
||||
|
||||
basemd5 = hashlib.md5()
|
||||
basemd5.update(base_rom_bytes)
|
||||
|
||||
+8
-15
@@ -11,6 +11,7 @@ from .Regions import create_regions, connect_regions
|
||||
from .Levels import level_list
|
||||
from .Rules import set_rules
|
||||
from .Names import ItemName, LocationName
|
||||
from .Client import DKC3SNIClient
|
||||
from ..AutoWorld import WebWorld, World
|
||||
from .Rom import LocalRom, patch_rom, get_base_rom_path, DKC3DeltaPatch
|
||||
import Patch
|
||||
@@ -65,10 +66,6 @@ class DKC3World(World):
|
||||
"active_levels": self.active_level_list,
|
||||
}
|
||||
|
||||
def _create_items(self, name: str):
|
||||
data = item_table[name]
|
||||
return [self.create_item(name)] * data.quantity
|
||||
|
||||
def fill_slot_data(self) -> dict:
|
||||
slot_data = self._get_slot_data()
|
||||
for option_name in dkc3_options:
|
||||
@@ -113,17 +110,17 @@ class DKC3World(World):
|
||||
number_of_bonus_coins = (self.world.krematoa_bonus_coin_cost[self.player] * 5)
|
||||
number_of_bonus_coins += math.ceil((85 - number_of_bonus_coins) * self.world.percentage_of_extra_bonus_coins[self.player] / 100)
|
||||
|
||||
itempool += [self.create_item(ItemName.bonus_coin)] * number_of_bonus_coins
|
||||
itempool += [self.create_item(ItemName.dk_coin)] * 41
|
||||
itempool += [self.create_item(ItemName.banana_bird)] * number_of_banana_birds
|
||||
itempool += [self.create_item(ItemName.krematoa_cog)] * number_of_cogs
|
||||
itempool += [self.create_item(ItemName.progressive_boat)] * 3
|
||||
itempool += [self.create_item(ItemName.bonus_coin) for _ in range(number_of_bonus_coins)]
|
||||
itempool += [self.create_item(ItemName.dk_coin) for _ in range(41)]
|
||||
itempool += [self.create_item(ItemName.banana_bird) for _ in range(number_of_banana_birds)]
|
||||
itempool += [self.create_item(ItemName.krematoa_cog) for _ in range(number_of_cogs)]
|
||||
itempool += [self.create_item(ItemName.progressive_boat) for _ in range(3)]
|
||||
|
||||
total_junk_count = total_required_locations - len(itempool)
|
||||
|
||||
junk_pool = []
|
||||
for item_name in self.world.random.choices(list(junk_table.keys()), k=total_junk_count):
|
||||
junk_pool += [self.create_item(item_name)]
|
||||
junk_pool.append(self.create_item(item_name))
|
||||
|
||||
itempool += junk_pool
|
||||
|
||||
@@ -146,11 +143,7 @@ class DKC3World(World):
|
||||
|
||||
self.active_level_list.append(LocationName.rocket_rush_region)
|
||||
|
||||
outfilepname = f'_P{player}'
|
||||
outfilepname += f"_{world.player_name[player].replace(' ', '_')}" \
|
||||
if world.player_name[player] != 'Player%d' % player else ''
|
||||
|
||||
rompath = os.path.join(output_directory, f'AP_{world.seed_name}{outfilepname}.sfc')
|
||||
rompath = os.path.join(output_directory, f"{self.world.get_out_file_name_base(self.player)}.sfc")
|
||||
rom.write_to_file(rompath)
|
||||
self.rom_name = rom.name
|
||||
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
|
||||
- Hardware or software capable of loading and playing SNES ROM files
|
||||
- An emulator capable of connecting to SNI such as:
|
||||
- snes9x Multitroid
|
||||
from: [snes9x Multitroid Download](https://drive.google.com/drive/folders/1_ej-pwWtCAHYXIrvs5Hro16A1s9Hi3Jz),
|
||||
- snes9x-rr from: [snes9x rr](https://github.com/gocha/snes9x-rr/releases),
|
||||
- BizHawk from: [BizHawk Website](http://tasvideos.org/BizHawk.html)
|
||||
- RetroArch 1.10.3 or newer from: [RetroArch Website](https://retroarch.com?page=platforms). Or,
|
||||
- An SD2SNES, FXPak Pro ([FXPak Pro Store Page](https://krikzz.com/store/home/54-fxpak-pro.html)), or other
|
||||
@@ -81,7 +80,7 @@ client, and will also create your ROM in the same place as your patch file.
|
||||
When the client launched automatically, SNI should have also automatically launched in the background. If this is its
|
||||
first time launching, you may be prompted to allow it to communicate through the Windows Firewall.
|
||||
|
||||
##### snes9x Multitroid
|
||||
##### snes9x-rr
|
||||
|
||||
1. Load your ROM file if it hasn't already been loaded.
|
||||
2. Click on the File menu and hover on **Lua Scripting**
|
||||
|
||||
@@ -11,6 +11,8 @@ import shutil
|
||||
|
||||
import Utils
|
||||
import Patch
|
||||
import worlds.AutoWorld
|
||||
import worlds.Files
|
||||
from . import Options
|
||||
|
||||
from .Technologies import tech_table, recipes, free_sample_exclusions, progressive_technology_table, \
|
||||
@@ -34,7 +36,8 @@ base_info = {
|
||||
"factorio_version": "1.1",
|
||||
"dependencies": [
|
||||
"base >= 1.1.0",
|
||||
"? science-not-invited"
|
||||
"? science-not-invited",
|
||||
"? factory-levels"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -56,7 +59,7 @@ recipe_time_ranges = {
|
||||
}
|
||||
|
||||
|
||||
class FactorioModFile(Patch.APContainer):
|
||||
class FactorioModFile(worlds.Files.APContainer):
|
||||
game = "Factorio"
|
||||
compression_method = zipfile.ZIP_DEFLATED # Factorio can't load LZMA archives
|
||||
|
||||
|
||||
@@ -7,7 +7,8 @@
|
||||
"description": "Integration client for the Archipelago Randomizer",
|
||||
"factorio_version": "1.1",
|
||||
"dependencies": [
|
||||
"base >= 1.1.0",
|
||||
"? science-not-invited"
|
||||
]
|
||||
"base >= 1.1.0",
|
||||
"? science-not-invited",
|
||||
"? factory-levels"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -157,6 +157,7 @@ function on_player_created(event)
|
||||
{%- if silo == 2 %}
|
||||
check_spawn_silo(game.players[event.player_index].force)
|
||||
{%- endif %}
|
||||
dumpInfo(player.force)
|
||||
end
|
||||
script.on_event(defines.events.on_player_created, on_player_created)
|
||||
|
||||
@@ -491,6 +492,7 @@ commands.add_command("ap-sync", "Used by the Archipelago client to get progress
|
||||
["death_link"] = DEATH_LINK,
|
||||
["energy"] = chain_lookup(forcedata, "energy"),
|
||||
["energy_bridges"] = chain_lookup(forcedata, "energy_bridges"),
|
||||
["multiplayer"] = #game.players > 1,
|
||||
}
|
||||
|
||||
for tech_name, tech in pairs(force.technologies) do
|
||||
@@ -596,5 +598,13 @@ commands.add_command("ap-energylink", "Used by the Archipelago client to manage
|
||||
global.forcedata[force].energy = global.forcedata[force].energy + change
|
||||
end)
|
||||
|
||||
commands.add_command("toggle-ap-send-filter", "Toggle filtering of item sends that get displayed in-game to only those that involve you.", function(call)
|
||||
log("Player command toggle-ap-send-filter") -- notifies client
|
||||
end)
|
||||
|
||||
commands.add_command("toggle-ap-chat", "Toggle sending of chat messages from players on the Factorio server to Archipelago.", function(call)
|
||||
log("Player command toggle-ap-chat") -- notifies client
|
||||
end)
|
||||
|
||||
-- data
|
||||
progressive_technologies = {{ dict_to_lua(progressive_technology_table) }}
|
||||
|
||||
@@ -183,6 +183,18 @@ end
|
||||
data.raw["assembling-machine"]["assembling-machine-1"].crafting_categories = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-3"].crafting_categories)
|
||||
data.raw["assembling-machine"]["assembling-machine-2"].crafting_categories = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-3"].crafting_categories)
|
||||
data.raw["assembling-machine"]["assembling-machine-1"].fluid_boxes = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-2"].fluid_boxes)
|
||||
if mods["factory-levels"] then
|
||||
-- Factory-Levels allows the assembling machines to get faster (and depending on settings), more productive at crafting products, the more the
|
||||
-- assembling machine crafts the product. If the machine crafts enough, it may auto-upgrade to the next tier.
|
||||
for i = 1, 25, 1 do
|
||||
data.raw["assembling-machine"]["assembling-machine-1-level-" .. i].crafting_categories = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-3"].crafting_categories)
|
||||
data.raw["assembling-machine"]["assembling-machine-1-level-" .. i].fluid_boxes = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-2"].fluid_boxes)
|
||||
end
|
||||
for i = 1, 50, 1 do
|
||||
data.raw["assembling-machine"]["assembling-machine-2-level-" .. i].crafting_categories = table.deepcopy(data.raw["assembling-machine"]["assembling-machine-3"].crafting_categories)
|
||||
end
|
||||
end
|
||||
|
||||
data.raw["ammo"]["artillery-shell"].stack_size = 10
|
||||
|
||||
{# each randomized tech gets set to be invisible, with new nodes added that trigger those #}
|
||||
|
||||
@@ -118,7 +118,7 @@ This allows you to host your own Factorio game.
|
||||
3. Install the mod into your Factorio Client by copying the zip file into the `mods` folder, which is likely located
|
||||
at `C:\Users\YourName\AppData\Roaming\Factorio\mods`.
|
||||
4. Obtain the Archipelago Server address from the website's host room, or from the server host.
|
||||
5. Run your Archipelago Client, which is named `ArchilepagoFactorioClient.exe`. This was installed along with
|
||||
5. Run your Archipelago Client, which is named `ArchipelagoFactorioClient.exe`. This was installed along with
|
||||
Archipelago if you chose to include it during the installation process.
|
||||
6. Enter `/connect [server-address]` into the input box at the bottom of the Archipelago Client and press "Enter"
|
||||
|
||||
@@ -132,6 +132,8 @@ This allows you to host your own Factorio game.
|
||||
|
||||
For additional client features, issue the `/help` command in the Archipelago Client. Once connected to the AP server,
|
||||
you can also issue the `!help` command to learn about additional commands like `!hint`.
|
||||
For more information about the commands you can use, see the [Commands Guide](/tutorial/Archipelago/commands/en) and
|
||||
[Other Settings](#other-settings).
|
||||
|
||||
## Allowing Other People to Join Your Game
|
||||
|
||||
@@ -141,19 +143,34 @@ you can also issue the `!help` command to learn about additional commands like `
|
||||
4. Provide your IP address to anyone you want to join your game, and have them follow the steps for
|
||||
"Connecting to Someone Else's Factorio Game" above.
|
||||
|
||||
## Other Settings
|
||||
|
||||
- By default, all item sends are displayed in-game. In larger async seeds this may become overly spammy.
|
||||
To hide all item sends that are not to or from your factory, do one of the following:
|
||||
- Type `/toggle-ap-send-filter` in-game
|
||||
- Type `/toggle_send_filter` in the Archipelago Client
|
||||
- In your `host.yaml` set
|
||||
```
|
||||
factorio_options:
|
||||
filter_item_sends: true
|
||||
```
|
||||
- By default, in-game chat is bridged to Archipelago. If you prefer to be able to speak privately, you can disable this
|
||||
feature by doing one of the following:
|
||||
- Type `/toggle-ap-chat` in-game
|
||||
- Type `/toggle_chat` in the Archipelago Client
|
||||
- In your `host.yaml` set
|
||||
```
|
||||
factorio_options:
|
||||
bridge_chat_out: false
|
||||
```
|
||||
Note that this will also disable `!` commands from within the game, and that it will not affect incoming chat.
|
||||
|
||||
## Troubleshooting
|
||||
|
||||
In case any problems should occur, the Archipelago Client will create a file `FactorioClient.txt` in the `/logs`. The
|
||||
contents of this file may help you troubleshoot an issue on your own and is vital for requesting help from other people
|
||||
in Archipelago.
|
||||
|
||||
## Commands in game
|
||||
|
||||
Once you have connected to the server successfully using the Archipelago Factorio Client you should see a message
|
||||
stating you can get help using Archipelago commands by typing `!help`. Commands cannot currently be sent from within
|
||||
the Factorio session, but you can send them from the Archipelago Factorio Client. For more information about the commands
|
||||
you can use see the [commands guide](/tutorial/Archipelago/commands/en).
|
||||
|
||||
## Additional Resources
|
||||
|
||||
- Alternate Tutorial by
|
||||
|
||||
+84
-25
@@ -1,6 +1,7 @@
|
||||
import collections
|
||||
import typing
|
||||
|
||||
from BaseClasses import LocationProgressType
|
||||
from BaseClasses import LocationProgressType, MultiWorld
|
||||
|
||||
if typing.TYPE_CHECKING:
|
||||
import BaseClasses
|
||||
@@ -12,39 +13,82 @@ else:
|
||||
ItemRule = typing.Callable[[object], bool]
|
||||
|
||||
|
||||
def group_locality_rules(world):
|
||||
def locality_needed(world: MultiWorld) -> bool:
|
||||
for player in world.player_ids:
|
||||
if world.local_items[player].value:
|
||||
return True
|
||||
if world.non_local_items[player].value:
|
||||
return True
|
||||
|
||||
# Group
|
||||
for group_id, group in world.groups.items():
|
||||
if set(world.player_ids) == set(group["players"]):
|
||||
continue
|
||||
if group["local_items"]:
|
||||
for location in world.get_locations():
|
||||
if location.player not in group["players"]:
|
||||
forbid_items_for_player(location, group["local_items"], group_id)
|
||||
return True
|
||||
if group["non_local_items"]:
|
||||
for location in world.get_locations():
|
||||
if location.player in group["players"]:
|
||||
forbid_items_for_player(location, group["non_local_items"], group_id)
|
||||
return True
|
||||
|
||||
|
||||
def locality_rules(world, player: int):
|
||||
if world.local_items[player].value:
|
||||
def locality_rules(world: MultiWorld):
|
||||
if locality_needed(world):
|
||||
|
||||
forbid_data: typing.Dict[int, typing.Dict[int, typing.Set[str]]] = \
|
||||
collections.defaultdict(lambda: collections.defaultdict(set))
|
||||
|
||||
def forbid(sender: int, receiver: int, items: typing.Set[str]):
|
||||
forbid_data[sender][receiver].update(items)
|
||||
|
||||
for receiving_player in world.player_ids:
|
||||
local_items: typing.Set[str] = world.local_items[receiving_player].value
|
||||
if local_items:
|
||||
for sending_player in world.player_ids:
|
||||
if receiving_player != sending_player:
|
||||
forbid(sending_player, receiving_player, local_items)
|
||||
non_local_items: typing.Set[str] = world.non_local_items[receiving_player].value
|
||||
if non_local_items:
|
||||
forbid(receiving_player, receiving_player, non_local_items)
|
||||
|
||||
# Group
|
||||
for receiving_group_id, receiving_group in world.groups.items():
|
||||
if set(world.player_ids) == set(receiving_group["players"]):
|
||||
continue
|
||||
if receiving_group["local_items"]:
|
||||
for sending_player in world.player_ids:
|
||||
if sending_player not in receiving_group["players"]:
|
||||
forbid(sending_player, receiving_group_id, receiving_group["local_items"])
|
||||
if receiving_group["non_local_items"]:
|
||||
for sending_player in world.player_ids:
|
||||
if sending_player in receiving_group["players"]:
|
||||
forbid(sending_player, receiving_group_id, receiving_group["non_local_items"])
|
||||
|
||||
# create fewer lambda's to save memory and cache misses
|
||||
func_cache = {}
|
||||
for location in world.get_locations():
|
||||
if location.player != player:
|
||||
forbid_items_for_player(location, world.local_items[player].value, player)
|
||||
if world.non_local_items[player].value:
|
||||
for location in world.get_locations():
|
||||
if location.player == player:
|
||||
forbid_items_for_player(location, world.non_local_items[player].value, player)
|
||||
if (location.player, location.item_rule) in func_cache:
|
||||
location.item_rule = func_cache[location.player, location.item_rule]
|
||||
# empty rule that just returns True, overwrite
|
||||
elif location.item_rule is location.__class__.item_rule:
|
||||
func_cache[location.player, location.item_rule] = location.item_rule = \
|
||||
lambda i, sending_blockers = forbid_data[location.player], \
|
||||
old_rule = location.item_rule: \
|
||||
i.name not in sending_blockers[i.player]
|
||||
# special rule, needs to also be fulfilled.
|
||||
else:
|
||||
func_cache[location.player, location.item_rule] = location.item_rule = \
|
||||
lambda i, sending_blockers = forbid_data[location.player], \
|
||||
old_rule = location.item_rule: \
|
||||
i.name not in sending_blockers[i.player] and old_rule(i)
|
||||
|
||||
|
||||
def exclusion_rules(world, player: int, exclude_locations: typing.Set[str]):
|
||||
def exclusion_rules(world: MultiWorld, player: int, exclude_locations: typing.Set[str]) -> None:
|
||||
for loc_name in exclude_locations:
|
||||
try:
|
||||
location = world.get_location(loc_name, player)
|
||||
except KeyError as e: # failed to find the given location. Check if it's a legitimate location
|
||||
if loc_name not in world.worlds[player].location_name_to_id:
|
||||
raise Exception(f"Unable to exclude location {loc_name} in player {player}'s world.") from e
|
||||
else:
|
||||
else:
|
||||
add_item_rule(location, lambda i: not (i.advancement or i.useful))
|
||||
location.progress_type = LocationProgressType.EXCLUDED
|
||||
|
||||
@@ -53,17 +97,25 @@ def set_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"],
|
||||
spot.access_rule = rule
|
||||
|
||||
|
||||
def add_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"], rule: CollectionRule, combine='and'):
|
||||
def add_rule(spot: typing.Union["BaseClasses.Location", "BaseClasses.Entrance"], rule: CollectionRule, combine="and"):
|
||||
old_rule = spot.access_rule
|
||||
if combine == 'or':
|
||||
spot.access_rule = lambda state: rule(state) or old_rule(state)
|
||||
# empty rule, replace instead of add
|
||||
if old_rule is spot.__class__.access_rule:
|
||||
spot.access_rule = rule if combine == "and" else old_rule
|
||||
else:
|
||||
spot.access_rule = lambda state: rule(state) and old_rule(state)
|
||||
if combine == "and":
|
||||
spot.access_rule = lambda state: rule(state) and old_rule(state)
|
||||
else:
|
||||
spot.access_rule = lambda state: rule(state) or old_rule(state)
|
||||
|
||||
|
||||
def forbid_item(location: "BaseClasses.Location", item: str, player: int):
|
||||
old_rule = location.item_rule
|
||||
location.item_rule = lambda i: (i.name != item or i.player != player) and old_rule(i)
|
||||
# empty rule
|
||||
if old_rule is location.__class__.item_rule:
|
||||
location.item_rule = lambda i: i.name != item or i.player != player
|
||||
else:
|
||||
location.item_rule = lambda i: (i.name != item or i.player != player) and old_rule(i)
|
||||
|
||||
|
||||
def forbid_items_for_player(location: "BaseClasses.Location", items: typing.Set[str], player: int):
|
||||
@@ -77,9 +129,16 @@ def forbid_items(location: "BaseClasses.Location", items: typing.Set[str]):
|
||||
location.item_rule = lambda i: i.name not in items and old_rule(i)
|
||||
|
||||
|
||||
def add_item_rule(location: "BaseClasses.Location", rule: ItemRule):
|
||||
def add_item_rule(location: "BaseClasses.Location", rule: ItemRule, combine: str = "and"):
|
||||
old_rule = location.item_rule
|
||||
location.item_rule = lambda item: rule(item) and old_rule(item)
|
||||
# empty rule, replace instead of add
|
||||
if old_rule is location.__class__.item_rule:
|
||||
location.item_rule = rule if combine == "and" else old_rule
|
||||
else:
|
||||
if combine == "and":
|
||||
location.item_rule = lambda item: rule(item) and old_rule(item)
|
||||
else:
|
||||
location.item_rule = lambda item: rule(item) or old_rule(item)
|
||||
|
||||
|
||||
def item_in_locations(state: "BaseClasses.CollectionState", item: str, player: int,
|
||||
|
||||
@@ -106,7 +106,7 @@ settings. If a game can be rolled it **must** have a settings section even if it
|
||||
|
||||
Some options in Archipelago can be used by every game but must still be placed within the relevant game's section.
|
||||
|
||||
Currently, these options are `start_inventory`, `start_hints`, `local_items`, `non_local_items`, `start_location_hints`
|
||||
Currently, these options are `start_inventory`, `early_items`, `start_hints`, `local_items`, `non_local_items`, `start_location_hints`
|
||||
, `exclude_locations`, and various plando options.
|
||||
|
||||
See the plando guide for more info on plando options. Plando
|
||||
@@ -115,6 +115,8 @@ guide: [Archipelago Plando Guide](/tutorial/Archipelago/plando/en)
|
||||
* `start_inventory` will give any items defined here to you at the beginning of your game. The format for this must be
|
||||
the name as it appears in the game files and the amount you would like to start with. For example `Rupees(5): 6` which
|
||||
will give you 30 rupees.
|
||||
* `early_items` is formatted in the same way as `start_inventory` and will force the number of each item specified to be
|
||||
forced into locations that are reachable from the start, before obtaining any items.
|
||||
* `start_hints` gives you free server hints for the defined item/s at the beginning of the game allowing you to hint for
|
||||
the location without using any hint points.
|
||||
* `local_items` will force any items you want to be in your world instead of being in another world.
|
||||
@@ -172,6 +174,8 @@ A Link to the Past:
|
||||
- Quake
|
||||
non_local_items:
|
||||
- Moon Pearl
|
||||
early_items:
|
||||
Flute: 1
|
||||
start_location_hints:
|
||||
- Spike Cave
|
||||
priority_locations:
|
||||
@@ -235,6 +239,9 @@ Timespinner:
|
||||
* `local_items` forces the `Bombos`, `Ether`, and `Quake` medallions to all be placed within our own world, meaning we
|
||||
have to find it ourselves.
|
||||
* `non_local_items` forces the `Moon Pearl` to be placed in someone else's world, meaning we won't be able to find it.
|
||||
* `early_items` forces the `Flute` to be placed in a location that is available from the beginning of the game ("Sphere
|
||||
1"). Since it is not specified in `local_items` or `non_local_items`, it can be placed one of these locations in any
|
||||
world.
|
||||
* `start_location_hints` gives us a starting hint for the `Spike Cave` location available at the beginning of the
|
||||
multiworld that can be used for no cost.
|
||||
* `priority_locations` forces a progression item to be placed on the `Link's House` location.
|
||||
|
||||
@@ -2,23 +2,24 @@
|
||||
|
||||
## What is Plando?
|
||||
|
||||
The purposes of randomizers is to randomize the items in a game to give a new experience. Plando takes this concept and
|
||||
The purpose of randomizers is to randomize the items in a game to give a new experience. Plando takes this concept and
|
||||
changes it up by allowing you to plan out certain aspects of the game by placing certain items in certain locations,
|
||||
certain bosses in certain rooms, edit text for certain NPCs/signs, or even force certain region connections. Each of
|
||||
these options are going to be detailed separately as `item plando`, `boss plando`, `text plando`,
|
||||
and `connection plando`. Every game in archipelago supports item plando but the other plando options are only supported
|
||||
by certain games. Currently, only LTTP supports text and boss plando. Support for connection plando may vary.
|
||||
and `connection plando`. Every game in Archipelago supports item plando but the other plando options are only supported
|
||||
by certain games. Currently, only A Link to the Past supports text and boss plando. Support for connection plando may
|
||||
vary.
|
||||
|
||||
### Enabling Plando
|
||||
|
||||
On the website plando will already be enabled. If you will be generating the game locally plando features must be
|
||||
On the website, plando will already be enabled. If you will be generating the game locally, plando features must be
|
||||
enabled (opt-in).
|
||||
|
||||
* To opt-in go to the archipelago installation (default: `C:\ProgramData\Archipelago`), open the host.yaml with a text
|
||||
* To opt-in go to the Archipelago installation (default: `C:\ProgramData\Archipelago`), open `host.yaml` with a text
|
||||
editor and find the `plando_options` key. The available plando modules can be enabled by adding them after this such
|
||||
as
|
||||
`plando_options: bosses, items, texts, connections`.
|
||||
* You can add the necessary plando modules for your settings to the `requires` section of your yaml. Doing so will throw an error if the options that you need to generate properly are not enabled to ensure you will get the results you desire. Only enter in the plando modules that you are using here but it should look like:
|
||||
* You can add the necessary plando modules for your settings to the `requires` section of your YAML. Doing so will throw an error if the options that you need to generate properly are not enabled to ensure you will get the results you desire. Only enter in the plando modules that you are using here but it should look like:
|
||||
|
||||
```yaml
|
||||
requires:
|
||||
@@ -27,45 +28,45 @@ enabled (opt-in).
|
||||
```
|
||||
|
||||
## Item Plando
|
||||
Item plando allows a player to place an item in a specific location or specific locations, place multiple items into a
|
||||
Item plando allows a player to place an item in a specific location or specific locations, or place multiple items into a
|
||||
list of specific locations both in their own game or in another player's game.
|
||||
|
||||
* The options for item plando are `from_pool`, `world`, `percentage`, `force`, `count`, and either item and location, or items
|
||||
and locations.
|
||||
* The options for item plando are `from_pool`, `world`, `percentage`, `force`, `count`, and either `item` and
|
||||
`location`, or `items` and `locations`.
|
||||
* `from_pool` determines if the item should be taken *from* the item pool or *added* to it. This can be true or
|
||||
false and defaults to true if omitted.
|
||||
* `world` is the target world to place the item in.
|
||||
* It gets ignored if only one world is generated.
|
||||
* Can be a number, name, true, false, null, or a list. False is the default.
|
||||
* If a number is used it targets that slot or player number in the multiworld.
|
||||
* If a name is used it will target the world with that player name.
|
||||
* If set to true it will be any player's world besides your own.
|
||||
* If set to false it will target your own world.
|
||||
* If set to null it will target a random world in the multiworld.
|
||||
* If a number is used, it targets that slot or player number in the multiworld.
|
||||
* If a name is used, it will target the world with that player name.
|
||||
* If set to true, it will be any player's world besides your own.
|
||||
* If set to false, it will target your own world.
|
||||
* If set to null, it will target a random world in the multiworld.
|
||||
* If a list of names is used, it will target the games with the player names specified.
|
||||
* `force` determines whether the generator will fail if the item can't be placed in the location can be true, false,
|
||||
* `force` determines whether the generator will fail if the item can't be placed in the location. Can be true, false,
|
||||
or silent. Silent is the default.
|
||||
* If set to true the item must be placed and the generator will throw an error if it is unable to do so.
|
||||
* If set to false the generator will log a warning if the placement can't be done but will still generate.
|
||||
* If set to silent and the placement fails it will be ignored entirely.
|
||||
* If set to true, the item must be placed and the generator will throw an error if it is unable to do so.
|
||||
* If set to false, the generator will log a warning if the placement can't be done but will still generate.
|
||||
* If set to silent and the placement fails, it will be ignored entirely.
|
||||
* `percentage` is the percentage chance for the relevant block to trigger. This can be any value from 0 to 100 and
|
||||
if omitted will default to 100.
|
||||
* Single Placement is when you use a plando block to place a single item at a single location.
|
||||
* `item` is the item you would like to place and `location` is the location to place it.
|
||||
* Multi Placement uses a plando block to place multiple items in multiple locations until either list is exhausted.
|
||||
* `items` defines the items to use and a number letting you place multiple of it. You can use true instead of a number to have it use however many of that item are in your item pool.
|
||||
* `items` defines the items to use, each with a number for the amount. Using `true` instead of a number uses however many of that item are in your item pool.
|
||||
* `locations` is a list of possible locations those items can be placed in.
|
||||
* Using the multi placement method, placements are picked randomly.
|
||||
* Instead of a number, you can use true
|
||||
|
||||
* `count` can be used to set the maximum number of items placed from the block. The default is 1 if using `item` and False if using `items`
|
||||
* If a number is used it will try to place this number of items.
|
||||
* If set to false it will try to place as many items from the block as it can.
|
||||
* If `min` and `max` are defined, it will try to place a number of items between these two numbers at random
|
||||
* If a number is used, it will try to place this number of items.
|
||||
* If set to false, it will try to place as many items from the block as it can.
|
||||
* If `min` and `max` are defined, it will try to place a number of items between these two numbers at random.
|
||||
|
||||
|
||||
### Available Items and Locations
|
||||
|
||||
A list of all available items and locations can be found in the [website's datapackage](/datapackage). The items and locations will be in the `"item_name_to_id"` and `"location_name_to_id"` sections of the relevant game. You do not need the quotes but the name must be entered in the same as it appears on that page and is caps-sensitive.
|
||||
A list of all available items and locations can be found in the [website's datapackage](/datapackage). The items and locations will be in the `"item_name_to_id"` and `"location_name_to_id"` sections of the relevant game. You do not need the quotes but the name must be entered in the same as it appears on that page and is case-sensitive.
|
||||
|
||||
### Examples
|
||||
|
||||
@@ -142,43 +143,43 @@ plando_items:
|
||||
min: 1
|
||||
max: 4
|
||||
```
|
||||
1. This block has a 50% chance to occur, and if it does will place either the Empire Orb or Radiant Orb on another player's
|
||||
Starter Chest 1 and removes the chosen item from the item pool.
|
||||
1. This block has a 50% chance to occur, and if it does, it will place either the Empire Orb or Radiant Orb on another
|
||||
player's Starter Chest 1 and removes the chosen item from the item pool.
|
||||
2. This block will always trigger and will place the player's swords, bow, magic meter, strength upgrades, and hookshots
|
||||
in their own dungeon major item chests.
|
||||
3. This block will always trigger and will lock boss relics on the bosses.
|
||||
4. This block has an 80% chance of occurring and when it does will place all but 1 of the items randomly among the four
|
||||
locations chosen here.
|
||||
4. This block has an 80% chance of occurring, and when it does, it will place all but 1 of the items randomly among the
|
||||
four locations chosen here.
|
||||
5. This block will always trigger and will attempt to place a random 2 of Levitate, Revealer and Energize into
|
||||
other players' Master Sword Pedestals or Boss Relic 1 locations.
|
||||
6. This block will always trigger and will attempt to place a random number, between 1 and 4, of progressive swords
|
||||
into any locations within the game slots named BobsSlaytheSpire and BobsRogueLegacy
|
||||
into any locations within the game slots named BobsSlaytheSpire and BobsRogueLegacy.
|
||||
|
||||
|
||||
## Boss Plando
|
||||
|
||||
As this is currently only supported by A Link to the Past instead of explaining here please refer to the
|
||||
As this is currently only supported by A Link to the Past, instead of finding an explanation here, please refer to the
|
||||
relevant guide: [A Link to the Past Plando Guide](/tutorial/A%20Link%20to%20the%20Past/plando/en)
|
||||
|
||||
## Text Plando
|
||||
|
||||
As this is currently only supported by A Link to the Past instead of explaining here please refer to the
|
||||
As this is currently only supported by A Link to the Past, instead of finding an explanation here, please refer to the
|
||||
relevant guide: [A Link to the Past Plando Guide](/tutorial/A%20Link%20to%20the%20Past/plando/en)
|
||||
|
||||
## Connections Plando
|
||||
|
||||
This is currently only supported by Minecraft and A Link to the Past. As the way that these games interact with their
|
||||
connections is different I will only explain the basics here while more specifics for Link to the Past connection plando
|
||||
can be found in its plando guide.
|
||||
connections is different, I will only explain the basics here, while more specifics for A Link to the Past connection
|
||||
plando can be found in its plando guide.
|
||||
|
||||
* The options for connections are `percentage`, `entrance`, `exit`, and `direction`. Each of these options support
|
||||
* The options for connections are `percentage`, `entrance`, `exit`, and `direction`. Each of these options supports
|
||||
subweights.
|
||||
* `percentage` is the percentage chance for this connection from 0 to 100 and defaults to 100.
|
||||
* Every connection has an `entrance` and an `exit`. These can be unlinked like in A Link to the Past insanity entrance
|
||||
shuffle.
|
||||
* `direction` can be `both`, `entrance`, or `exit` and determines in which direction this connection will operate.
|
||||
|
||||
[Link to the Past connections](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/EntranceShuffle.py#L3852)
|
||||
[A Link to the Past connections](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/alttp/EntranceShuffle.py#L3852)
|
||||
|
||||
[Minecraft connections](https://github.com/ArchipelagoMW/Archipelago/blob/main/worlds/minecraft/Regions.py#L62)
|
||||
|
||||
@@ -186,7 +187,7 @@ can be found in its plando guide.
|
||||
|
||||
```yaml
|
||||
plando_connections:
|
||||
# example block 1 - Link to the Past
|
||||
# example block 1 - A Link to the Past
|
||||
- entrance: Cave Shop (Lake Hylia)
|
||||
exit: Cave 45
|
||||
direction: entrance
|
||||
@@ -206,9 +207,9 @@ plando_connections:
|
||||
direction: both
|
||||
```
|
||||
|
||||
1. These connections are decoupled so going into the lake hylia cave shop will take you to the inside of cave 45 and
|
||||
when you leave the interior you will exit to the cave 45 ledge. Going into the cave 45 entrance will then take you to
|
||||
the lake hylia cave shop. Walking into the entrance for the old man cave and Agahnim's Tower entrance will both take
|
||||
you to their locations as normal but leaving old man cave will exit at Agahnim's Tower.
|
||||
2. This will force a nether fortress and a village to be the overworld structures for your game. Note that for the
|
||||
1. These connections are decoupled, so going into the Lake Hylia Cave Shop will take you to the inside of Cave 45, and
|
||||
when you leave the interior, you will exit to the Cave 45 ledge. Going into the Cave 45 entrance will then take you to
|
||||
the Lake Hylia Cave Shop. Walking into the entrance for the Old Man Cave and Agahnim's Tower entrance will both take
|
||||
you to their locations as normal, but leaving Old Man Cave will exit at Agahnim's Tower.
|
||||
2. This will force a Nether fortress and a village to be the Overworld structures for your game. Note that for the
|
||||
Minecraft connection plando to work structure shuffle must be enabled.
|
||||
|
||||
@@ -8,36 +8,31 @@ about 5 minutes to read.
|
||||
|
||||
Triggers allow you to customize your game settings by allowing you to define one or many options which only occur under
|
||||
specific conditions. These are essentially "if, then" statements for options in your game. A good example of what you
|
||||
can do with triggers is the custom mercenary mode YAML that was created using entirely triggers and plando.
|
||||
can do with triggers is the [custom mercenary mode YAML
|
||||
](https://github.com/alwaysintreble/Archipelago-yaml-dump/blob/main/Snippets/Mercenary%20Mode%20Snippet.yaml) that was
|
||||
created using entirely triggers and plando.
|
||||
|
||||
Mercenary mode
|
||||
YAML: [Mercenary Mode YAML on GitHub](https://github.com/alwaysintreble/Archipelago-yaml-dump/blob/main/Snippets/Mercenary%20Mode%20Snippet.yaml)
|
||||
|
||||
For more information on plando you can reference the general plando guide or the Link to the Past plando guide.
|
||||
|
||||
General plando guide: [Archipelago Plando Guide](/tutorial/Archipelago/plando/en)
|
||||
|
||||
Link to the Past plando guide: [LttP Plando Guide](/tutorial/A%20Link%20to%20the%20Past/plando/en)
|
||||
For more information on plando, you can reference the [general plando guide](/tutorial/Archipelago/plando/en) or the
|
||||
[A Link to the Past plando guide](/tutorial/A%20Link%20to%20the%20Past/plando/en).
|
||||
|
||||
## Trigger use
|
||||
|
||||
Triggers may be defined in either the root or in the relevant game sections. Generally, The best place to do this is the
|
||||
bottom of the yaml for clear organization.
|
||||
Triggers may be defined in either the root or in the relevant game sections. Generally, the best place to do this is the
|
||||
bottom of the YAML for clear organization.
|
||||
|
||||
- Triggers comprise the trigger section and then each trigger must have an `option_category`, `option_name`, and
|
||||
`option_result` from which it will react to and then an `options` section for the definition of what will happen.
|
||||
- `option_category` is the defining section from which the option is defined in.
|
||||
Each trigger consists of four parts:
|
||||
- `option_category` specifies the section which the triggering option is defined in.
|
||||
- Example: `A Link to the Past`
|
||||
- This is the root category the option is located in. If the option you're triggering off of is in root then you
|
||||
- This is the category the option is located in. If the option you're triggering off of is in root then you
|
||||
would use `null`, otherwise this is the game for which you want this option trigger to activate.
|
||||
- `option_name` is the option setting from which the triggered choice is going to react to.
|
||||
- `option_name` specifies the name of the triggering option.
|
||||
- Example: `shop_item_slots`
|
||||
- This can be any option from any category defined in the yaml file in either root or a game section.
|
||||
- `option_result` is the result of this option setting from which you would like to react.
|
||||
- This can be any option from any category defined in the YAML file in either root or a game section.
|
||||
- `option_result` specifies the value of the option that activates this trigger.
|
||||
- Example: `15`
|
||||
- Each trigger must be used for exactly one option result. If you would like the same thing to occur with multiple
|
||||
results you would need multiple triggers for this.
|
||||
- `options` is where you define what will happen when this is detected. This can be something as simple as ensuring
|
||||
results, you would need multiple triggers for this.
|
||||
- `options` is where you define what will happen when the trigger activates. This can be something as simple as ensuring
|
||||
another option also gets selected or placing an item in a certain location. It is possible to have multiple things
|
||||
happen in this section.
|
||||
- Example:
|
||||
@@ -47,10 +42,10 @@ bottom of the yaml for clear organization.
|
||||
Rupees (300): 2
|
||||
```
|
||||
|
||||
This format must be:
|
||||
The general format is:
|
||||
|
||||
```yaml
|
||||
root option:
|
||||
category:
|
||||
option to change:
|
||||
desired result
|
||||
```
|
||||
@@ -70,8 +65,8 @@ The above examples all together will end up looking like this:
|
||||
Rupees(300): 2
|
||||
```
|
||||
|
||||
For this example if the generator happens to roll 15 shuffled in shop item slots for your game you'll be granted 600
|
||||
rupees at the beginning. These can also be used to change other options.
|
||||
For this example, if the generator happens to roll 15 shuffled in shop item slots for your game, you'll be granted 600
|
||||
rupees at the beginning. Triggers can also be used to change other options.
|
||||
|
||||
For example:
|
||||
|
||||
@@ -85,9 +80,9 @@ For example:
|
||||
Inverted: true
|
||||
```
|
||||
|
||||
In this example if your world happens to roll SpecificKeycards then your game will also start in inverted.
|
||||
In this example, if your world happens to roll SpecificKeycards, then your game will also start in inverted.
|
||||
|
||||
It is also possible to use imaginary names in options to trigger specific settings. You can use these made up names in
|
||||
It is also possible to use imaginary values in options to trigger specific settings. You can use these made-up values in
|
||||
either your main options or to trigger from another trigger. Currently, this is the only way to trigger on "setting 1
|
||||
AND setting 2".
|
||||
|
||||
@@ -97,33 +92,33 @@ For example:
|
||||
triggers:
|
||||
- option_category: Secret of Evermore
|
||||
option_name: doggomizer
|
||||
option_result: pupdunk
|
||||
options:
|
||||
Secret of Evermore:
|
||||
difficulty:
|
||||
normal: 50
|
||||
pupdunk_hard: 25
|
||||
pupdunk_mystery: 25
|
||||
exp_modifier:
|
||||
150: 50
|
||||
200: 50
|
||||
- option_category: Secret of Evermore
|
||||
option_name: difficulty
|
||||
option_result: pupdunk_hard
|
||||
options:
|
||||
Secret of Evermore:
|
||||
fix_wings_glitch: false
|
||||
difficulty: hard
|
||||
- option_category: Secret of Evermore
|
||||
option_name: difficulty
|
||||
option_result: pupdunk_mystery
|
||||
options:
|
||||
Secret of Evermore:
|
||||
fix_wings_glitch: false
|
||||
difficulty: mystery
|
||||
option_result: pupdunk
|
||||
options:
|
||||
Secret of Evermore:
|
||||
difficulty:
|
||||
normal: 50
|
||||
pupdunk_hard: 25
|
||||
pupdunk_mystery: 25
|
||||
exp_modifier:
|
||||
150: 50
|
||||
200: 50
|
||||
- option_category: Secret of Evermore
|
||||
option_name: difficulty
|
||||
option_result: pupdunk_hard
|
||||
options:
|
||||
Secret of Evermore:
|
||||
fix_wings_glitch: false
|
||||
difficulty: hard
|
||||
- option_category: Secret of Evermore
|
||||
option_name: difficulty
|
||||
option_result: pupdunk_mystery
|
||||
options:
|
||||
Secret of Evermore:
|
||||
fix_wings_glitch: false
|
||||
difficulty: mystery
|
||||
```
|
||||
|
||||
In this example (thanks to @Black-Sliver) if the `pupdunk` option is rolled then the difficulty values will be rolled
|
||||
In this example (thanks to @Black-Sliver), if the `pupdunk` option is rolled, then the difficulty values will be rolled
|
||||
again using the new options `normal`, `pupdunk_hard`, and `pupdunk_mystery`, and the exp modifier will be rerolled using
|
||||
new weights for 150 and 200. This allows for two more triggers that will only be used for the new `pupdunk_hard`
|
||||
and `pupdunk_mystery` options so that they will only be triggered on "pupdunk AND hard/mystery".
|
||||
@@ -637,7 +637,7 @@ class HKItem(Item):
|
||||
def __init__(self, name, advancement, code, type: str, player: int = None):
|
||||
if name == "Mimic_Grub":
|
||||
classification = ItemClassification.trap
|
||||
elif type in ("Grub", "DreamWarrior", "Root", "Egg"):
|
||||
elif type in ("Grub", "DreamWarrior", "Root", "Egg", "Dreamer"):
|
||||
classification = ItemClassification.progression_skip_balancing
|
||||
elif type == "Charm" and name not in progression_charms:
|
||||
classification = ItemClassification.progression_skip_balancing
|
||||
|
||||
@@ -0,0 +1,94 @@
|
||||
from typing import List, Dict
|
||||
|
||||
|
||||
region_exit_table: Dict[int, List[str]] = {
|
||||
0: ["New Game"],
|
||||
|
||||
1: ["To Waynehouse",
|
||||
"To New Muldul",
|
||||
"To Viewax",
|
||||
"To TV Island",
|
||||
"To Shield Facility",
|
||||
"To Worm Pod",
|
||||
"To Foglast",
|
||||
"To Sage Labyrinth",
|
||||
"To Hylemxylem"],
|
||||
|
||||
2: ["To World",
|
||||
"To Afterlife",],
|
||||
|
||||
3: ["To Airship",
|
||||
"To Waynehouse",
|
||||
"To New Muldul",
|
||||
"To Drill Castle",
|
||||
"To Viewax",
|
||||
"To Arcade Island",
|
||||
"To TV Island",
|
||||
"To Juice Ranch",
|
||||
"To Shield Facility",
|
||||
"To Worm Pod",
|
||||
"To Foglast",
|
||||
"To Sage Airship",
|
||||
"To Hylemxylem"],
|
||||
|
||||
4: ["To World",
|
||||
"To Afterlife",
|
||||
"To New Muldul Vault"],
|
||||
|
||||
5: ["To New Muldul"],
|
||||
|
||||
6: ["To World",
|
||||
"To Afterlife"],
|
||||
|
||||
7: ["To World"],
|
||||
|
||||
8: ["To World"],
|
||||
|
||||
9: ["To World",
|
||||
"To Afterlife"],
|
||||
|
||||
10: ["To World"],
|
||||
|
||||
11: ["To World",
|
||||
"To Afterlife",
|
||||
"To Worm Pod"],
|
||||
|
||||
12: ["To Shield Facility",
|
||||
"To Afterlife"],
|
||||
|
||||
13: ["To World",
|
||||
"To Afterlife"],
|
||||
|
||||
14: ["To World",
|
||||
"To Sage Labyrinth"],
|
||||
|
||||
15: ["To Drill Castle",
|
||||
"To Afterlife"],
|
||||
|
||||
16: ["To World"],
|
||||
|
||||
17: ["To World",
|
||||
"To Afterlife"]
|
||||
}
|
||||
|
||||
|
||||
exit_lookup_table: Dict[str, int] = {
|
||||
"New Game": 2,
|
||||
"To Waynehouse": 2,
|
||||
"To Afterlife": 1,
|
||||
"To World": 3,
|
||||
"To New Muldul": 4,
|
||||
"To New Muldul Vault": 5,
|
||||
"To Viewax": 6,
|
||||
"To Airship": 7,
|
||||
"To Arcade Island": 8,
|
||||
"To TV Island": 9,
|
||||
"To Juice Ranch": 10,
|
||||
"To Shield Facility": 11,
|
||||
"To Worm Pod": 12,
|
||||
"To Foglast": 13,
|
||||
"To Drill Castle": 14,
|
||||
"To Sage Labyrinth": 15,
|
||||
"To Sage Airship": 16,
|
||||
"To Hylemxylem": 17
|
||||
}
|
||||
@@ -0,0 +1,243 @@
|
||||
from BaseClasses import ItemClassification
|
||||
from typing import TypedDict, Dict
|
||||
|
||||
|
||||
class ItemDict(TypedDict):
|
||||
classification: ItemClassification
|
||||
count: int
|
||||
name: str
|
||||
|
||||
|
||||
item_table: Dict[int, ItemDict] = {
|
||||
# Things
|
||||
200622: {'classification': ItemClassification.filler,
|
||||
'count': 1,
|
||||
'name': 'DUBIOUS BERRY'},
|
||||
200623: {'classification': ItemClassification.filler,
|
||||
'count': 11,
|
||||
'name': 'BURRITO'},
|
||||
200624: {'classification': ItemClassification.filler,
|
||||
'count': 1,
|
||||
'name': 'COFFEE'},
|
||||
200625: {'classification': ItemClassification.filler,
|
||||
'count': 6,
|
||||
'name': 'SOUL SPONGE'},
|
||||
200626: {'classification': ItemClassification.useful,
|
||||
'count': 6,
|
||||
'name': 'MUSCLE APPLIQUE'},
|
||||
200627: {'classification': ItemClassification.filler,
|
||||
'count': 1,
|
||||
'name': 'POOLWINE'},
|
||||
200628: {'classification': ItemClassification.filler,
|
||||
'count': 3,
|
||||
'name': 'CUPCAKE'},
|
||||
200629: {'classification': ItemClassification.filler,
|
||||
'count': 3,
|
||||
'name': 'COOKIE'},
|
||||
200630: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': 'HOUSE KEY'},
|
||||
200631: {'classification': ItemClassification.filler,
|
||||
'count': 2,
|
||||
'name': 'MEAT'},
|
||||
200632: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': 'PNEUMATOPHORE'},
|
||||
200633: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': 'CAVE KEY'},
|
||||
200634: {'classification': ItemClassification.filler,
|
||||
'count': 6,
|
||||
'name': 'JUICE'},
|
||||
200635: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': 'DOCK KEY'},
|
||||
200636: {'classification': ItemClassification.filler,
|
||||
'count': 14,
|
||||
'name': 'BANANA'},
|
||||
200637: {'classification': ItemClassification.progression,
|
||||
'count': 3,
|
||||
'name': 'PAPER CUP'},
|
||||
200638: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': 'JAIL KEY'},
|
||||
200639: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': 'PADDLE'},
|
||||
200640: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': 'WORM ROOM KEY'},
|
||||
200641: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': 'BRIDGE KEY'},
|
||||
200642: {'classification': ItemClassification.filler,
|
||||
'count': 2,
|
||||
'name': 'STEM CELL'},
|
||||
200643: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': 'UPPER CHAMBER KEY'},
|
||||
200644: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': 'VESSEL ROOM KEY'},
|
||||
200645: {'classification': ItemClassification.filler,
|
||||
'count': 3,
|
||||
'name': 'CLOUD GERM'},
|
||||
200646: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': 'SKULL BOMB'},
|
||||
200647: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': 'TOWER KEY'},
|
||||
200648: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': 'DEEP KEY'},
|
||||
200649: {'classification': ItemClassification.filler,
|
||||
'count': 1,
|
||||
'name': 'MULTI-COFFEE'},
|
||||
200650: {'classification': ItemClassification.filler,
|
||||
'count': 4,
|
||||
'name': 'MULTI-JUICE'},
|
||||
200651: {'classification': ItemClassification.filler,
|
||||
'count': 1,
|
||||
'name': 'MULTI STEM CELL'},
|
||||
200652: {'classification': ItemClassification.filler,
|
||||
'count': 6,
|
||||
'name': 'MULTI SOUL SPONGE'},
|
||||
#200653: {'classification': ItemClassification.filler,
|
||||
# 'count': 1,
|
||||
# 'name': 'ANTENNA'},
|
||||
200654: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': 'UPPER HOUSE KEY'},
|
||||
200655: {'classification': ItemClassification.useful,
|
||||
'count': 1,
|
||||
'name': 'BOTTOMLESS JUICE'},
|
||||
200656: {'classification': ItemClassification.progression,
|
||||
'count': 3,
|
||||
'name': 'SAGE TOKEN'},
|
||||
200657: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': 'CLICKER'},
|
||||
|
||||
# Garbs > Gloves
|
||||
200658: {'classification': ItemClassification.useful,
|
||||
'count': 1,
|
||||
'name': 'CURSED GLOVES'},
|
||||
200659: {'classification': ItemClassification.useful,
|
||||
'count': 5,
|
||||
'name': 'LONG GLOVES'},
|
||||
200660: {'classification': ItemClassification.useful,
|
||||
'count': 1,
|
||||
'name': 'BRAIN DIGITS'},
|
||||
200661: {'classification': ItemClassification.useful,
|
||||
'count': 1,
|
||||
'name': 'MATERIEL MITTS'},
|
||||
200662: {'classification': ItemClassification.useful,
|
||||
'count': 1,
|
||||
'name': 'PLEATHER GAGE'},
|
||||
200663: {'classification': ItemClassification.useful,
|
||||
'count': 1,
|
||||
'name': 'PEPTIDE BODKINS'},
|
||||
200664: {'classification': ItemClassification.useful,
|
||||
'count': 1,
|
||||
'name': 'TELESCOPIC SLEEVE'},
|
||||
200665: {'classification': ItemClassification.useful,
|
||||
'count': 1,
|
||||
'name': 'TENDRIL HAND'},
|
||||
200666: {'classification': ItemClassification.useful,
|
||||
'count': 1,
|
||||
'name': 'PSYCHIC KNUCKLE'},
|
||||
200667: {'classification': ItemClassification.useful,
|
||||
'count': 1,
|
||||
'name': 'SINGLE GLOVE'},
|
||||
|
||||
# Garbs > Accessories
|
||||
200668: {'classification': ItemClassification.useful,
|
||||
'count': 1,
|
||||
'name': 'FADED PONCHO'},
|
||||
200669: {'classification': ItemClassification.useful,
|
||||
'count': 1,
|
||||
'name': 'JUMPSUIT'},
|
||||
200670: {'classification': ItemClassification.useful,
|
||||
'count': 1,
|
||||
'name': 'BOOTS'},
|
||||
200671: {'classification': ItemClassification.useful,
|
||||
'count': 1,
|
||||
'name': 'CONVERTER WORM'},
|
||||
200672: {'classification': ItemClassification.useful,
|
||||
'count': 1,
|
||||
'name': 'COFFEE CHIP'},
|
||||
200673: {'classification': ItemClassification.useful,
|
||||
'count': 1,
|
||||
'name': 'RANCHER PONCHO'},
|
||||
200674: {'classification': ItemClassification.useful,
|
||||
'count': 1,
|
||||
'name': 'ORGAN FORT'},
|
||||
200675: {'classification': ItemClassification.useful,
|
||||
'count': 2,
|
||||
'name': 'LOOPED DOME'},
|
||||
200676: {'classification': ItemClassification.useful,
|
||||
'count': 1,
|
||||
'name': 'DUCTILE HABIT'},
|
||||
200677: {'classification': ItemClassification.useful,
|
||||
'count': 2,
|
||||
'name': 'TARP'},
|
||||
|
||||
# Bones
|
||||
200686: {'classification': ItemClassification.filler,
|
||||
'count': 1,
|
||||
'name': '100 Bones'},
|
||||
200687: {'classification': ItemClassification.filler,
|
||||
'count': 1,
|
||||
'name': '50 Bones'}
|
||||
}
|
||||
|
||||
|
||||
gesture_item_table: Dict[int, ItemDict] = {
|
||||
200678: {'classification': ItemClassification.useful,
|
||||
'count': 1,
|
||||
'name': 'POROMER BLEB'},
|
||||
200679: {'classification': ItemClassification.useful,
|
||||
'count': 1,
|
||||
'name': 'SOUL CRISPER'},
|
||||
200680: {'classification': ItemClassification.useful,
|
||||
'count': 1,
|
||||
'name': 'TIME SIGIL'},
|
||||
200681: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': 'CHARGE UP'},
|
||||
200682: {'classification': ItemClassification.useful,
|
||||
'count': 1,
|
||||
'name': 'FATE SANDBOX'},
|
||||
200683: {'classification': ItemClassification.useful,
|
||||
'count': 1,
|
||||
'name': 'TELEDENUDATE'},
|
||||
200684: {'classification': ItemClassification.useful,
|
||||
'count': 1,
|
||||
'name': 'LINK MOLLUSC'},
|
||||
200685: {'classification': ItemClassification.useful,
|
||||
'count': 1,
|
||||
'name': 'BOMBO - GENESIS'},
|
||||
200688: {'classification': ItemClassification.useful,
|
||||
'count': 1,
|
||||
'name': 'NEMATODE INTERFACE'},
|
||||
}
|
||||
|
||||
|
||||
party_item_table: Dict[int, ItemDict] = {
|
||||
200689: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': 'Pongorma'},
|
||||
200690: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': 'Dedusmuln'},
|
||||
200691: {'classification': ItemClassification.progression,
|
||||
'count': 1,
|
||||
'name': 'Somsnosa'}
|
||||
}
|
||||
|
||||
medallion_item_table: Dict[int, ItemDict] = {
|
||||
200692: {'classification': ItemClassification.filler,
|
||||
'count': 30,
|
||||
'name': '10 Bones'}
|
||||
}
|
||||
@@ -0,0 +1,383 @@
|
||||
from typing import Dict, TypedDict
|
||||
|
||||
|
||||
class LocationDict(TypedDict, total=False):
|
||||
name: str
|
||||
region: int
|
||||
|
||||
|
||||
location_table: Dict[int, LocationDict] = {
|
||||
# Waynehouse
|
||||
200622: {'name': "Waynehouse: Toilet",
|
||||
'region': 2},
|
||||
200623: {'name': "Waynehouse: Basement Pot 1",
|
||||
'region': 2},
|
||||
200624: {'name': "Waynehouse: Basement Pot 2",
|
||||
'region': 2},
|
||||
200625: {'name': "Waynehouse: Basement Pot 3",
|
||||
'region': 2},
|
||||
200626: {'name': "Waynehouse: Sarcophagus",
|
||||
'region': 2},
|
||||
|
||||
# Afterlife
|
||||
200628: {'name': "Afterlife: Mangled Wayne",
|
||||
'region': 1},
|
||||
200629: {'name': "Afterlife: Jar near Mangled Wayne",
|
||||
'region': 1},
|
||||
200630: {'name': "Afterlife: Jar under Pool",
|
||||
'region': 1},
|
||||
|
||||
# New Muldul
|
||||
200632: {'name': "New Muldul: Shop Ceiling Pot 1",
|
||||
'region': 4},
|
||||
200633: {'name': "New Muldul: Shop Ceiling Pot 2",
|
||||
'region': 4},
|
||||
200634: {'name': "New Muldul: Flag Banana",
|
||||
'region': 4},
|
||||
200635: {'name': "New Muldul: Pot near Vault",
|
||||
'region': 4},
|
||||
200636: {'name': "New Muldul: Underground Pot",
|
||||
'region': 4},
|
||||
200637: {'name': "New Muldul: Underground Chest",
|
||||
'region': 4},
|
||||
200638: {'name': "New Muldul: Juice Trade",
|
||||
'region': 4},
|
||||
200639: {'name': "New Muldul: Basement Suitcase",
|
||||
'region': 4},
|
||||
200640: {'name': "New Muldul: Upper House Chest 1",
|
||||
'region': 4},
|
||||
200641: {'name': "New Muldul: Upper House Chest 2",
|
||||
'region': 4},
|
||||
|
||||
# New Muldul Vault
|
||||
200643: {'name': "New Muldul: Talk to Pongorma",
|
||||
'region': 4},
|
||||
200645: {'name': "New Muldul: Rescued Blerol 1",
|
||||
'region': 4},
|
||||
200646: {'name': "New Muldul: Rescued Blerol 2",
|
||||
'region': 4},
|
||||
200647: {'name': "New Muldul: Vault Left Chest",
|
||||
'region': 5},
|
||||
200648: {'name': "New Muldul: Vault Right Chest",
|
||||
'region': 5},
|
||||
200649: {'name': "New Muldul: Vault Bomb",
|
||||
'region': 5},
|
||||
|
||||
# Viewax's Edifice
|
||||
200650: {'name': "Viewax's Edifice: Fountain Banana",
|
||||
'region': 6},
|
||||
200651: {'name': "Viewax's Edifice: Dedusmuln's Suitcase",
|
||||
'region': 6},
|
||||
200652: {'name': "Viewax's Edifice: Dedusmuln's Campfire",
|
||||
'region': 6},
|
||||
200653: {'name': "Viewax's Edifice: Talk to Dedusmuln",
|
||||
'region': 6},
|
||||
200655: {'name': "Viewax's Edifice: Canopic Jar",
|
||||
'region': 6},
|
||||
200656: {'name': "Viewax's Edifice: Cave Sarcophagus",
|
||||
'region': 6},
|
||||
200657: {'name': "Viewax's Edifice: Shielded Key",
|
||||
'region': 6},
|
||||
200658: {'name': "Viewax's Edifice: Tower Pot",
|
||||
'region': 6},
|
||||
200659: {'name': "Viewax's Edifice: Tower Jar",
|
||||
'region': 6},
|
||||
200660: {'name': "Viewax's Edifice: Tower Chest",
|
||||
'region': 6},
|
||||
200661: {'name': "Viewax's Edifice: Sage Fridge",
|
||||
'region': 6},
|
||||
200662: {'name': "Viewax's Edifice: Sage Item 1",
|
||||
'region': 6},
|
||||
200663: {'name': "Viewax's Edifice: Sage Item 2",
|
||||
'region': 6},
|
||||
200664: {'name': "Viewax's Edifice: Viewax Pot",
|
||||
'region': 6},
|
||||
200665: {'name': "Viewax's Edifice: Defeat Viewax",
|
||||
'region': 6},
|
||||
|
||||
# Viewax Arcade Minigame
|
||||
200667: {'name': "Arcade 1: Key",
|
||||
'region': 6},
|
||||
200668: {'name': "Arcade 1: Coin Dash",
|
||||
'region': 6},
|
||||
200669: {'name': "Arcade 1: Burrito Alcove 1",
|
||||
'region': 6},
|
||||
200670: {'name': "Arcade 1: Burrito Alcove 2",
|
||||
'region': 6},
|
||||
200671: {'name': "Arcade 1: Behind Spikes Banana",
|
||||
'region': 6},
|
||||
200672: {'name': "Arcade 1: Pyramid Banana",
|
||||
'region': 6},
|
||||
200673: {'name': "Arcade 1: Moving Platforms Muscle Applique",
|
||||
'region': 6},
|
||||
200674: {'name': "Arcade 1: Bed Banana",
|
||||
'region': 6},
|
||||
|
||||
# Airship
|
||||
200675: {'name': "Airship: Talk to Somsnosa",
|
||||
'region': 7},
|
||||
|
||||
# Arcade Island
|
||||
200676: {'name': "Arcade Island: Shielded Key",
|
||||
'region': 8},
|
||||
200677: {'name': "Arcade 2: Flying Machine Banana",
|
||||
'region': 8},
|
||||
200678: {'name': "Arcade 2: Paper Cup Detour",
|
||||
'region': 8},
|
||||
200679: {'name': "Arcade 2: Peak Muscle Applique",
|
||||
'region': 8},
|
||||
200680: {'name': "Arcade 2: Double Banana 1",
|
||||
'region': 8},
|
||||
200681: {'name': "Arcade 2: Double Banana 2",
|
||||
'region': 8},
|
||||
200682: {'name': "Arcade 2: Cave Burrito",
|
||||
'region': 8},
|
||||
|
||||
# Juice Ranch
|
||||
200684: {'name': "Juice Ranch: Juice 1",
|
||||
'region': 10},
|
||||
200685: {'name': "Juice Ranch: Juice 2",
|
||||
'region': 10},
|
||||
200686: {'name': "Juice Ranch: Juice 3",
|
||||
'region': 10},
|
||||
200687: {'name': "Juice Ranch: Ledge Rancher",
|
||||
'region': 10},
|
||||
200688: {'name': "Juice Ranch: Battle with Somsnosa",
|
||||
'region': 10},
|
||||
200690: {'name': "Juice Ranch: Fridge",
|
||||
'region': 10},
|
||||
|
||||
# Worm Pod
|
||||
200692: {'name': "Worm Pod: Key",
|
||||
'region': 12},
|
||||
|
||||
# Foglast
|
||||
200693: {'name': "Foglast: West Sarcophagus",
|
||||
'region': 13},
|
||||
200694: {'name': "Foglast: Underground Sarcophagus",
|
||||
'region': 13},
|
||||
200695: {'name': "Foglast: Shielded Key",
|
||||
'region': 13},
|
||||
200696: {'name': "Foglast: Buy Clicker",
|
||||
'region': 13},
|
||||
200698: {'name': "Foglast: Shielded Chest",
|
||||
'region': 13},
|
||||
200699: {'name': "Foglast: Cave Fridge",
|
||||
'region': 13},
|
||||
200700: {'name': "Foglast: Roof Sarcophagus",
|
||||
'region': 13},
|
||||
200701: {'name': "Foglast: Under Lair Sarcophagus 1",
|
||||
'region': 13},
|
||||
200702: {'name': "Foglast: Under Lair Sarcophagus 2",
|
||||
'region': 13},
|
||||
200703: {'name': "Foglast: Under Lair Sarcophagus 3",
|
||||
'region': 13},
|
||||
200704: {'name': "Foglast: Sage Sarcophagus",
|
||||
'region': 13},
|
||||
200705: {'name': "Foglast: Sage Item 1",
|
||||
'region': 13},
|
||||
200706: {'name': "Foglast: Sage Item 2",
|
||||
'region': 13},
|
||||
|
||||
# Drill Castle
|
||||
200707: {'name': "Drill Castle: Ledge Banana",
|
||||
'region': 14},
|
||||
200708: {'name': "Drill Castle: Island Banana",
|
||||
'region': 14},
|
||||
200709: {'name': "Drill Castle: Island Pot",
|
||||
'region': 14},
|
||||
200710: {'name': "Drill Castle: Cave Sarcophagus",
|
||||
'region': 14},
|
||||
200711: {'name': "Drill Castle: Roof Banana",
|
||||
'region': 14},
|
||||
|
||||
# Sage Labyrinth
|
||||
200713: {'name': "Sage Labyrinth: 1F Chest Near Fountain",
|
||||
'region': 15},
|
||||
200714: {'name': "Sage Labyrinth: 1F Hidden Sarcophagus",
|
||||
'region': 15},
|
||||
200715: {'name': "Sage Labyrinth: 1F Four Statues Chest 1",
|
||||
'region': 15},
|
||||
200716: {'name': "Sage Labyrinth: 1F Four Statues Chest 2",
|
||||
'region': 15},
|
||||
200717: {'name': "Sage Labyrinth: B1 Double Chest 1",
|
||||
'region': 15},
|
||||
200718: {'name': "Sage Labyrinth: B1 Double Chest 2",
|
||||
'region': 15},
|
||||
200719: {'name': "Sage Labyrinth: B1 Single Chest",
|
||||
'region': 15},
|
||||
200720: {'name': "Sage Labyrinth: B1 Enemy Chest",
|
||||
'region': 15},
|
||||
200721: {'name': "Sage Labyrinth: B1 Hidden Sarcophagus",
|
||||
'region': 15},
|
||||
200722: {'name': "Sage Labyrinth: B1 Hole Chest",
|
||||
'region': 15},
|
||||
200723: {'name': "Sage Labyrinth: B2 Hidden Sarcophagus 1",
|
||||
'region': 15},
|
||||
200724: {'name': "Sage Labyrinth: B2 Hidden Sarcophagus 2",
|
||||
'region': 15},
|
||||
200754: {'name': "Sage Labyrinth: 2F Sarcophagus",
|
||||
'region': 15},
|
||||
200725: {'name': "Sage Labyrinth: Motor Hunter Sarcophagus",
|
||||
'region': 15},
|
||||
200726: {'name': "Sage Labyrinth: Sage Item 1",
|
||||
'region': 15},
|
||||
200727: {'name': "Sage Labyrinth: Sage Item 2",
|
||||
'region': 15},
|
||||
200728: {'name': "Sage Labyrinth: Sage Left Arm",
|
||||
'region': 15},
|
||||
200729: {'name': "Sage Labyrinth: Sage Right Arm",
|
||||
'region': 15},
|
||||
200730: {'name': "Sage Labyrinth: Sage Left Leg",
|
||||
'region': 15},
|
||||
200731: {'name': "Sage Labyrinth: Sage Right Leg",
|
||||
'region': 15},
|
||||
|
||||
# Sage Airship
|
||||
200732: {'name': "Sage Airship: Bottom Level Pot",
|
||||
'region': 16},
|
||||
200733: {'name': "Sage Airship: Flesh Pot",
|
||||
'region': 16},
|
||||
200734: {'name': "Sage Airship: Top Jar",
|
||||
'region': 16},
|
||||
|
||||
# Hylemxylem
|
||||
200736: {'name': "Hylemxylem: Jar",
|
||||
'region': 17},
|
||||
200737: {'name': "Hylemxylem: Lower Reservoir Key",
|
||||
'region': 17},
|
||||
200738: {'name': "Hylemxylem: Fountain Banana",
|
||||
'region': 17},
|
||||
200739: {'name': "Hylemxylem: East Island Banana",
|
||||
'region': 17},
|
||||
200740: {'name': "Hylemxylem: East Island Chest",
|
||||
'region': 17},
|
||||
200741: {'name': "Hylemxylem: Upper Chamber Banana",
|
||||
'region': 17},
|
||||
200742: {'name': "Hylemxylem: Across Upper Reservoir Chest",
|
||||
'region': 17},
|
||||
200743: {'name': "Hylemxylem: Drained Lower Reservoir Chest",
|
||||
'region': 17},
|
||||
200744: {'name': "Hylemxylem: Drained Lower Reservoir Burrito 1",
|
||||
'region': 17},
|
||||
200745: {'name': "Hylemxylem: Drained Lower Reservoir Burrito 2",
|
||||
'region': 17},
|
||||
200746: {'name': "Hylemxylem: Lower Reservoir Hole Pot 1",
|
||||
'region': 17},
|
||||
200747: {'name': "Hylemxylem: Lower Reservoir Hole Pot 2",
|
||||
'region': 17},
|
||||
200748: {'name': "Hylemxylem: Lower Reservoir Hole Pot 3",
|
||||
'region': 17},
|
||||
200749: {'name': "Hylemxylem: Lower Reservoir Hole Sarcophagus",
|
||||
'region': 17},
|
||||
200750: {'name': "Hylemxylem: Drained Upper Reservoir Burrito 1",
|
||||
'region': 17},
|
||||
200751: {'name': "Hylemxylem: Drained Upper Reservoir Burrito 2",
|
||||
'region': 17},
|
||||
200752: {'name': "Hylemxylem: Drained Upper Reservoir Burrito 3",
|
||||
'region': 17},
|
||||
200753: {'name': "Hylemxylem: Upper Reservoir Hole Key",
|
||||
'region': 17}
|
||||
}
|
||||
|
||||
|
||||
tv_location_table: Dict[int, LocationDict] = {
|
||||
200627: {'name': "Waynehouse: TV",
|
||||
'region': 2},
|
||||
200631: {'name': "Afterlife: TV",
|
||||
'region': 1},
|
||||
200642: {'name': "New Muldul: TV",
|
||||
'region': 4},
|
||||
200666: {'name': "Viewax's Edifice: TV",
|
||||
'region': 6},
|
||||
200683: {'name': "TV Island: TV",
|
||||
'region': 9},
|
||||
200691: {'name': "Juice Ranch: TV",
|
||||
'region': 10},
|
||||
200697: {'name': "Foglast: TV",
|
||||
'region': 13},
|
||||
200712: {'name': "Drill Castle: TV",
|
||||
'region': 14},
|
||||
200735: {'name': "Sage Airship: TV",
|
||||
'region': 16}
|
||||
}
|
||||
|
||||
|
||||
party_location_table: Dict[int, LocationDict] = {
|
||||
200644: {'name': "New Muldul: Pongorma Joins",
|
||||
'region': 4},
|
||||
200654: {'name': "Viewax's Edifice: Dedusmuln Joins",
|
||||
'region': 6},
|
||||
200689: {'name': "Juice Ranch: Somsnosa Joins",
|
||||
'region': 10}
|
||||
}
|
||||
|
||||
|
||||
medallion_location_table: Dict[int, LocationDict] = {
|
||||
200755: {'name': "New Muldul: Upper House Medallion",
|
||||
'region': 4},
|
||||
|
||||
200756: {'name': "New Muldul: Vault Rear Left Medallion",
|
||||
'region': 5},
|
||||
200757: {'name': "New Muldul: Vault Rear Right Medallion",
|
||||
'region': 5},
|
||||
200758: {'name': "New Muldul: Vault Center Medallion",
|
||||
'region': 5},
|
||||
200759: {'name': "New Muldul: Vault Front Left Medallion",
|
||||
'region': 5},
|
||||
200760: {'name': "New Muldul: Vault Front Right Medallion",
|
||||
'region': 5},
|
||||
|
||||
200761: {'name': "Viewax's Edifice: Fort Wall Medallion",
|
||||
'region': 6},
|
||||
200762: {'name': "Viewax's Edifice: Jar Medallion",
|
||||
'region': 6},
|
||||
200763: {'name': "Viewax's Edifice: Sage Chair Medallion",
|
||||
'region': 6},
|
||||
200764: {'name': "Arcade 1: Lonely Medallion",
|
||||
'region': 6},
|
||||
200765: {'name': "Arcade 1: Alcove Medallion",
|
||||
'region': 6},
|
||||
200766: {'name': "Arcade 1: Lava Medallion",
|
||||
'region': 6},
|
||||
|
||||
200767: {'name': "Arcade 2: Flying Machine Medallion",
|
||||
'region': 8},
|
||||
200768: {'name': "Arcade 2: Guarded Medallion",
|
||||
'region': 8},
|
||||
200769: {'name': "Arcade 2: Spinning Medallion",
|
||||
'region': 8},
|
||||
200770: {'name': "Arcade 2: Hook Medallion",
|
||||
'region': 8},
|
||||
200771: {'name': "Arcade 2: Flag Medallion",
|
||||
'region': 8},
|
||||
|
||||
200772: {'name': "Foglast: Under Lair Medallion",
|
||||
'region': 13},
|
||||
200773: {'name': "Foglast: Mid-Air Medallion",
|
||||
'region': 13},
|
||||
200774: {'name': "Foglast: Top of Tower Medallion",
|
||||
'region': 13},
|
||||
|
||||
200775: {'name': "Sage Airship: Walkway Medallion",
|
||||
'region': 16},
|
||||
200776: {'name': "Sage Airship: Flesh Medallion",
|
||||
'region': 16},
|
||||
200777: {'name': "Sage Airship: Top of Ship Medallion",
|
||||
'region': 16},
|
||||
200778: {'name': "Sage Airship: Hidden Medallion 1",
|
||||
'region': 16},
|
||||
200779: {'name': "Sage Airship: Hidden Medallion 2",
|
||||
'region': 16},
|
||||
200780: {'name': "Sage Airship: Hidden Medallion 3",
|
||||
'region': 16},
|
||||
|
||||
200781: {'name': "Hylemxylem: Lower Reservoir Medallion",
|
||||
'region': 17},
|
||||
200782: {'name': "Hylemxylem: Lower Reservoir Hole Medallion",
|
||||
'region': 17},
|
||||
200783: {'name': "Hylemxylem: Drain Switch Medallion",
|
||||
'region': 17},
|
||||
200784: {'name': "Hylemxylem: Warpo Medallion",
|
||||
'region': 17}
|
||||
}
|
||||
@@ -0,0 +1,41 @@
|
||||
from Options import Choice, Toggle, DefaultOnToggle, DeathLink
|
||||
|
||||
class PartyShuffle(Toggle):
|
||||
"""Shuffles party members into the pool.
|
||||
Note that enabling this can potentially increase both the difficulty and length of a run."""
|
||||
display_name = "Shuffle Party Members"
|
||||
|
||||
class GestureShuffle(Choice):
|
||||
"""Choose where gestures will appear in the item pool."""
|
||||
display_name = "Shuffle Gestures"
|
||||
option_anywhere = 0
|
||||
option_tvs_only = 1
|
||||
option_default_locations = 2
|
||||
default = 0
|
||||
|
||||
class MedallionShuffle(Toggle):
|
||||
"""Shuffles red medallions into the pool."""
|
||||
display_name = "Shuffle Red Medallions"
|
||||
|
||||
class RandomStart(Toggle):
|
||||
"""Start the randomizer in 1 of 4 positions.
|
||||
(Waynehouse, Viewax's Edifice, TV Island, Shield Facility)"""
|
||||
display_name = "Randomize Start Location"
|
||||
|
||||
class ExtraLogic(DefaultOnToggle):
|
||||
"""Include some extra items in logic (CHARGE UP, 1x PAPER CUP) to prevent the game from becoming too difficult."""
|
||||
display_name = "Extra Items in Logic"
|
||||
|
||||
class Hylics2DeathLink(DeathLink):
|
||||
"""When you die, everyone dies. The reverse is also true.
|
||||
Note that this also includes death by using the PERISH gesture.
|
||||
Can be toggled via in-game console command "/deathlink"."""
|
||||
|
||||
hylics2_options = {
|
||||
"party_shuffle": PartyShuffle,
|
||||
"gesture_shuffle" : GestureShuffle,
|
||||
"medallion_shuffle" : MedallionShuffle,
|
||||
"random_start" : RandomStart,
|
||||
"extra_items_in_logic": ExtraLogic,
|
||||
"death_link": Hylics2DeathLink
|
||||
}
|
||||
@@ -0,0 +1,433 @@
|
||||
from worlds.generic.Rules import add_rule
|
||||
from ..AutoWorld import LogicMixin
|
||||
|
||||
|
||||
class Hylics2Logic(LogicMixin):
|
||||
|
||||
def _hylics2_can_air_dash(self, player):
|
||||
return self.has("PNEUMATOPHORE", player)
|
||||
|
||||
def _hylics2_has_airship(self, player):
|
||||
return self.has("DOCK KEY", player)
|
||||
|
||||
def _hylics2_has_jail_key(self, player):
|
||||
return self.has("JAIL KEY", player)
|
||||
|
||||
def _hylics2_has_paddle(self, player):
|
||||
return self.has("PADDLE", player)
|
||||
|
||||
def _hylics2_has_worm_room_key(self, player):
|
||||
return self.has("WORM ROOM KEY", player)
|
||||
|
||||
def _hylics2_has_bridge_key(self, player):
|
||||
return self.has("BRIDGE KEY", player)
|
||||
|
||||
def _hylics2_has_upper_chamber_key(self, player):
|
||||
return self.has("UPPER CHAMBER KEY", player)
|
||||
|
||||
def _hylics2_has_vessel_room_key(self, player):
|
||||
return self.has("VESSEL ROOM KEY", player)
|
||||
|
||||
def _hylics2_has_house_key(self, player):
|
||||
return self.has("HOUSE KEY", player)
|
||||
|
||||
def _hylics2_has_cave_key(self, player):
|
||||
return self.has("CAVE KEY", player)
|
||||
|
||||
def _hylics2_has_skull_bomb(self, player):
|
||||
return self.has("SKULL BOMB", player)
|
||||
|
||||
def _hylics2_has_tower_key(self, player):
|
||||
return self.has("TOWER KEY", player)
|
||||
|
||||
def _hylics2_has_deep_key(self, player):
|
||||
return self.has("DEEP KEY", player)
|
||||
|
||||
def _hylics2_has_upper_house_key(self, player):
|
||||
return self.has("UPPER HOUSE KEY", player)
|
||||
|
||||
def _hylics2_has_clicker(self, player):
|
||||
return self.has("CLICKER", player)
|
||||
|
||||
def _hylics2_has_tokens(self, player):
|
||||
return self.has("SAGE TOKEN", player, 3)
|
||||
|
||||
def _hylics2_has_charge_up(self, player):
|
||||
return self.has("CHARGE UP", player)
|
||||
|
||||
def _hylics2_has_cup(self, player):
|
||||
return self.has("PAPER CUP", player, 1)
|
||||
|
||||
def _hylics2_has_1_member(self, player):
|
||||
return self.has("Pongorma", player) or self.has("Dedusmuln", player) or self.has("Somsnosa", player)
|
||||
|
||||
def _hylics2_has_2_members(self, player):
|
||||
return (self.has("Pongorma", player) and self.has("Dedusmuln", player)) or\
|
||||
(self.has("Pongorma", player) and self.has("Somsnosa", player)) or\
|
||||
(self.has("Dedusmuln", player) and self.has("Somsnosa", player))
|
||||
|
||||
def _hylics2_has_3_members(self, player):
|
||||
return self.has("Pongorma", player) and self.has("Dedusmuln", player) and self.has("Somsnosa", player)
|
||||
|
||||
def _hylics2_enter_arcade2(self, player):
|
||||
return self._hylics2_can_air_dash(player) and self._hylics2_has_airship(player)
|
||||
|
||||
def _hylics2_enter_wormpod(self, player):
|
||||
return self._hylics2_has_airship(player) and self._hylics2_has_worm_room_key(player) and\
|
||||
self._hylics2_has_paddle(player)
|
||||
|
||||
def _hylics2_enter_sageship(self, player):
|
||||
return self._hylics2_has_skull_bomb(player) and self._hylics2_has_airship(player) and\
|
||||
self._hylics2_has_paddle(player)
|
||||
|
||||
def _hylics2_enter_foglast(self, player):
|
||||
return self._hylics2_enter_wormpod(player)
|
||||
|
||||
def _hylics2_enter_hylemxylem(self, player):
|
||||
return self._hylics2_can_air_dash(player) and self._hylics2_enter_wormpod(player) and\
|
||||
self._hylics2_has_bridge_key(player)
|
||||
|
||||
|
||||
def set_rules(hylics2world):
|
||||
world = hylics2world.world
|
||||
player = hylics2world.player
|
||||
|
||||
# Afterlife
|
||||
add_rule(world.get_location("Afterlife: TV", player),
|
||||
lambda state: state._hylics2_has_cave_key(player))
|
||||
|
||||
# New Muldul
|
||||
add_rule(world.get_location("New Muldul: Underground Chest", player),
|
||||
lambda state: state._hylics2_can_air_dash(player))
|
||||
add_rule(world.get_location("New Muldul: TV", player),
|
||||
lambda state: state._hylics2_has_house_key(player))
|
||||
add_rule(world.get_location("New Muldul: Upper House Chest 1", player),
|
||||
lambda state: state._hylics2_has_upper_house_key(player))
|
||||
add_rule(world.get_location("New Muldul: Upper House Chest 2", player),
|
||||
lambda state: state._hylics2_has_upper_house_key(player))
|
||||
|
||||
# New Muldul Vault
|
||||
add_rule(world.get_location("New Muldul: Rescued Blerol 1", player),
|
||||
lambda state: (state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player)) and\
|
||||
((state._hylics2_has_jail_key(player) and state._hylics2_has_paddle(player)) or\
|
||||
(state._hylics2_has_bridge_key(player) and state._hylics2_has_worm_room_key(player))))
|
||||
add_rule(world.get_location("New Muldul: Rescued Blerol 2", player),
|
||||
lambda state: (state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player)) and\
|
||||
((state._hylics2_has_jail_key(player) and state._hylics2_has_paddle(player)) or\
|
||||
(state._hylics2_has_bridge_key(player) and state._hylics2_has_worm_room_key(player))))
|
||||
add_rule(world.get_location("New Muldul: Vault Left Chest", player),
|
||||
lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player))
|
||||
add_rule(world.get_location("New Muldul: Vault Right Chest", player),
|
||||
lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player))
|
||||
add_rule(world.get_location("New Muldul: Vault Bomb", player),
|
||||
lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player))
|
||||
|
||||
# Viewax's Edifice
|
||||
add_rule(world.get_location("Viewax's Edifice: Canopic Jar", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
add_rule(world.get_location("Viewax's Edifice: Cave Sarcophagus", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
add_rule(world.get_location("Viewax's Edifice: Shielded Key", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
add_rule(world.get_location("Viewax's Edifice: Shielded Key", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
add_rule(world.get_location("Viewax's Edifice: Tower Pot", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
add_rule(world.get_location("Viewax's Edifice: Tower Jar", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
add_rule(world.get_location("Viewax's Edifice: Tower Chest", player),
|
||||
lambda state: state._hylics2_has_paddle(player) and state._hylics2_has_tower_key(player))
|
||||
add_rule(world.get_location("Viewax's Edifice: Viewax Pot", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
add_rule(world.get_location("Viewax's Edifice: Defeat Viewax", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
add_rule(world.get_location("Viewax's Edifice: TV", player),
|
||||
lambda state: state._hylics2_has_paddle(player) and state._hylics2_has_jail_key(player))
|
||||
add_rule(world.get_location("Viewax's Edifice: Sage Fridge", player),
|
||||
lambda state: state._hylics2_can_air_dash(player))
|
||||
add_rule(world.get_location("Viewax's Edifice: Sage Item 1", player),
|
||||
lambda state: state._hylics2_can_air_dash(player))
|
||||
add_rule(world.get_location("Viewax's Edifice: Sage Item 2", player),
|
||||
lambda state: state._hylics2_can_air_dash(player))
|
||||
|
||||
# Arcade 1
|
||||
add_rule(world.get_location("Arcade 1: Key", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
add_rule(world.get_location("Arcade 1: Coin Dash", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
add_rule(world.get_location("Arcade 1: Burrito Alcove 1", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
add_rule(world.get_location("Arcade 1: Burrito Alcove 2", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
add_rule(world.get_location("Arcade 1: Behind Spikes Banana", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
add_rule(world.get_location("Arcade 1: Pyramid Banana", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
add_rule(world.get_location("Arcade 1: Moving Platforms Muscle Applique", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
add_rule(world.get_location("Arcade 1: Bed Banana", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
|
||||
# Airship
|
||||
add_rule(world.get_location("Airship: Talk to Somsnosa", player),
|
||||
lambda state: state._hylics2_has_worm_room_key(player))
|
||||
|
||||
# Foglast
|
||||
add_rule(world.get_location("Foglast: Underground Sarcophagus", player),
|
||||
lambda state: state._hylics2_can_air_dash(player))
|
||||
add_rule(world.get_location("Foglast: Shielded Key", player),
|
||||
lambda state: state._hylics2_can_air_dash(player))
|
||||
add_rule(world.get_location("Foglast: TV", player),
|
||||
lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_clicker(player))
|
||||
add_rule(world.get_location("Foglast: Buy Clicker", player),
|
||||
lambda state: state._hylics2_can_air_dash(player))
|
||||
add_rule(world.get_location("Foglast: Shielded Chest", player),
|
||||
lambda state: state._hylics2_can_air_dash(player))
|
||||
add_rule(world.get_location("Foglast: Cave Fridge", player),
|
||||
lambda state: state._hylics2_can_air_dash(player))
|
||||
add_rule(world.get_location("Foglast: Roof Sarcophagus", player),
|
||||
lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player))
|
||||
add_rule(world.get_location("Foglast: Under Lair Sarcophagus 1", player),
|
||||
lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player))
|
||||
add_rule(world.get_location("Foglast: Under Lair Sarcophagus 2", player),
|
||||
lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player))
|
||||
add_rule(world.get_location("Foglast: Under Lair Sarcophagus 3", player),
|
||||
lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player))
|
||||
add_rule(world.get_location("Foglast: Sage Sarcophagus", player),
|
||||
lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player))
|
||||
add_rule(world.get_location("Foglast: Sage Item 1", player),
|
||||
lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player))
|
||||
add_rule(world.get_location("Foglast: Sage Item 2", player),
|
||||
lambda state: state._hylics2_can_air_dash(player) and state._hylics2_has_bridge_key(player))
|
||||
|
||||
# Drill Castle
|
||||
add_rule(world.get_location("Drill Castle: Island Banana", player),
|
||||
lambda state: state._hylics2_can_air_dash(player))
|
||||
add_rule(world.get_location("Drill Castle: Island Pot", player),
|
||||
lambda state: state._hylics2_can_air_dash(player))
|
||||
add_rule(world.get_location("Drill Castle: Cave Sarcophagus", player),
|
||||
lambda state: state._hylics2_can_air_dash(player))
|
||||
add_rule(world.get_location("Drill Castle: TV", player),
|
||||
lambda state: state._hylics2_can_air_dash(player))
|
||||
|
||||
# Sage Labyrinth
|
||||
add_rule(world.get_location("Sage Labyrinth: Sage Item 1", player),
|
||||
lambda state: state._hylics2_has_deep_key(player))
|
||||
add_rule(world.get_location("Sage Labyrinth: Sage Item 2", player),
|
||||
lambda state: state._hylics2_has_deep_key(player))
|
||||
add_rule(world.get_location("Sage Labyrinth: Sage Left Arm", player),
|
||||
lambda state: state._hylics2_has_deep_key(player))
|
||||
add_rule(world.get_location("Sage Labyrinth: Sage Right Arm", player),
|
||||
lambda state: state._hylics2_has_deep_key(player))
|
||||
add_rule(world.get_location("Sage Labyrinth: Sage Left Leg", player),
|
||||
lambda state: state._hylics2_has_deep_key(player))
|
||||
add_rule(world.get_location("Sage Labyrinth: Sage Right Leg", player),
|
||||
lambda state: state._hylics2_has_deep_key(player))
|
||||
|
||||
# Sage Airship
|
||||
add_rule(world.get_location("Sage Airship: TV", player),
|
||||
lambda state: state._hylics2_has_tokens(player))
|
||||
|
||||
# Hylemxylem
|
||||
add_rule(world.get_location("Hylemxylem: Upper Chamber Banana", player),
|
||||
lambda state: state._hylics2_has_upper_chamber_key(player))
|
||||
add_rule(world.get_location("Hylemxylem: Across Upper Reservoir Chest", player),
|
||||
lambda state: state._hylics2_has_upper_chamber_key(player))
|
||||
add_rule(world.get_location("Hylemxylem: Drained Lower Reservoir Chest", player),
|
||||
lambda state: state._hylics2_has_upper_chamber_key(player))
|
||||
add_rule(world.get_location("Hylemxylem: Drained Lower Reservoir Burrito 1", player),
|
||||
lambda state: state._hylics2_has_upper_chamber_key(player))
|
||||
add_rule(world.get_location("Hylemxylem: Drained Lower Reservoir Burrito 2", player),
|
||||
lambda state: state._hylics2_has_upper_chamber_key(player))
|
||||
add_rule(world.get_location("Hylemxylem: Lower Reservoir Hole Pot 1", player),
|
||||
lambda state: state._hylics2_has_upper_chamber_key(player))
|
||||
add_rule(world.get_location("Hylemxylem: Lower Reservoir Hole Pot 2", player),
|
||||
lambda state: state._hylics2_has_upper_chamber_key(player))
|
||||
add_rule(world.get_location("Hylemxylem: Lower Reservoir Hole Pot 3", player),
|
||||
lambda state: state._hylics2_has_upper_chamber_key(player))
|
||||
add_rule(world.get_location("Hylemxylem: Lower Reservoir Hole Sarcophagus", player),
|
||||
lambda state: state._hylics2_has_upper_chamber_key(player))
|
||||
add_rule(world.get_location("Hylemxylem: Drained Upper Reservoir Burrito 1", player),
|
||||
lambda state: state._hylics2_has_upper_chamber_key(player))
|
||||
add_rule(world.get_location("Hylemxylem: Drained Upper Reservoir Burrito 2", player),
|
||||
lambda state: state._hylics2_has_upper_chamber_key(player))
|
||||
add_rule(world.get_location("Hylemxylem: Drained Upper Reservoir Burrito 3", player),
|
||||
lambda state: state._hylics2_has_upper_chamber_key(player))
|
||||
add_rule(world.get_location("Hylemxylem: Upper Reservoir Hole Key", player),
|
||||
lambda state: state._hylics2_has_upper_chamber_key(player))
|
||||
|
||||
# extra rules if Extra Items in Logic is enabled
|
||||
if world.extra_items_in_logic[player]:
|
||||
for i in world.get_region("Foglast", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_charge_up(player))
|
||||
for i in world.get_region("Sage Airship", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_charge_up(player) and state._hylics2_has_cup(player) and\
|
||||
state._hylics2_has_worm_room_key(player))
|
||||
for i in world.get_region("Hylemxylem", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_charge_up(player) and state._hylics2_has_cup(player))
|
||||
|
||||
add_rule(world.get_location("Sage Labyrinth: Motor Hunter Sarcophagus", player),
|
||||
lambda state: state._hylics2_has_charge_up(player) and state._hylics2_has_cup(player))
|
||||
|
||||
# extra rules if Shuffle Party Members is enabled
|
||||
if world.party_shuffle[player]:
|
||||
for i in world.get_region("Arcade Island", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_3_members(player))
|
||||
for i in world.get_region("Foglast", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_3_members(player) or\
|
||||
(state._hylics2_has_2_members(player) and state._hylics2_has_jail_key(player)))
|
||||
for i in world.get_region("Sage Airship", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_3_members(player))
|
||||
for i in world.get_region("Hylemxylem", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_3_members(player))
|
||||
|
||||
add_rule(world.get_location("Viewax's Edifice: Defeat Viewax", player),
|
||||
lambda state: state._hylics2_has_2_members(player))
|
||||
add_rule(world.get_location("New Muldul: Rescued Blerol 1", player),
|
||||
lambda state: state._hylics2_has_2_members(player))
|
||||
add_rule(world.get_location("New Muldul: Rescued Blerol 2", player),
|
||||
lambda state: state._hylics2_has_2_members(player))
|
||||
add_rule(world.get_location("New Muldul: Vault Left Chest", player),
|
||||
lambda state: state._hylics2_has_3_members(player))
|
||||
add_rule(world.get_location("New Muldul: Vault Right Chest", player),
|
||||
lambda state: state._hylics2_has_3_members(player))
|
||||
add_rule(world.get_location("New Muldul: Vault Bomb", player),
|
||||
lambda state: state._hylics2_has_2_members(player))
|
||||
add_rule(world.get_location("Juice Ranch: Battle with Somsnosa", player),
|
||||
lambda state: state._hylics2_has_2_members(player))
|
||||
add_rule(world.get_location("Juice Ranch: Somsnosa Joins", player),
|
||||
lambda state: state._hylics2_has_2_members(player))
|
||||
add_rule(world.get_location("Airship: Talk to Somsnosa", player),
|
||||
lambda state: state._hylics2_has_3_members(player))
|
||||
add_rule(world.get_location("Sage Labyrinth: Motor Hunter Sarcophagus", player),
|
||||
lambda state: state._hylics2_has_3_members(player))
|
||||
|
||||
# extra rules if Shuffle Red Medallions is enabled
|
||||
if world.medallion_shuffle[player]:
|
||||
add_rule(world.get_location("New Muldul: Upper House Medallion", player),
|
||||
lambda state: state._hylics2_has_upper_house_key(player))
|
||||
add_rule(world.get_location("New Muldul: Vault Rear Left Medallion", player),
|
||||
lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player))
|
||||
add_rule(world.get_location("New Muldul: Vault Rear Right Medallion", player),
|
||||
lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player))
|
||||
add_rule(world.get_location("New Muldul: Vault Center Medallion", player),
|
||||
lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player))
|
||||
add_rule(world.get_location("New Muldul: Vault Front Left Medallion", player),
|
||||
lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player))
|
||||
add_rule(world.get_location("New Muldul: Vault Front Right Medallion", player),
|
||||
lambda state: state._hylics2_enter_foglast(player) and state._hylics2_has_bridge_key(player))
|
||||
add_rule(world.get_location("Viewax's Edifice: Fort Wall Medallion", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
add_rule(world.get_location("Viewax's Edifice: Jar Medallion", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
add_rule(world.get_location("Viewax's Edifice: Sage Chair Medallion", player),
|
||||
lambda state: state._hylics2_can_air_dash(player))
|
||||
add_rule(world.get_location("Arcade 1: Lonely Medallion", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
add_rule(world.get_location("Arcade 1: Alcove Medallion", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
add_rule(world.get_location("Foglast: Under Lair Medallion", player),
|
||||
lambda state: state._hylics2_has_bridge_key(player))
|
||||
add_rule(world.get_location("Foglast: Mid-Air Medallion", player),
|
||||
lambda state: state._hylics2_can_air_dash(player))
|
||||
add_rule(world.get_location("Foglast: Top of Tower Medallion", player),
|
||||
lambda state: state._hylics2_has_paddle(player))
|
||||
add_rule(world.get_location("Hylemxylem: Lower Reservoir Hole Medallion", player),
|
||||
lambda state: state._hylics2_has_upper_chamber_key(player))
|
||||
|
||||
# extra rules is Shuffle Red Medallions and Party Shuffle are enabled
|
||||
if world.party_shuffle[player] and world.medallion_shuffle[player]:
|
||||
add_rule(world.get_location("New Muldul: Vault Rear Left Medallion", player),
|
||||
lambda state: state._hylics2_has_jail_key(player))
|
||||
add_rule(world.get_location("New Muldul: Vault Rear Right Medallion", player),
|
||||
lambda state: state._hylics2_has_jail_key(player))
|
||||
add_rule(world.get_location("New Muldul: Vault Center Medallion", player),
|
||||
lambda state: state._hylics2_has_jail_key(player))
|
||||
add_rule(world.get_location("New Muldul: Vault Front Left Medallion", player),
|
||||
lambda state: state._hylics2_has_jail_key(player))
|
||||
add_rule(world.get_location("New Muldul: Vault Front Right Medallion", player),
|
||||
lambda state: state._hylics2_has_jail_key(player))
|
||||
|
||||
# entrances
|
||||
for i in world.get_region("Airship", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
for i in world.get_region("Arcade Island", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player) and state._hylics2_can_air_dash(player))
|
||||
for i in world.get_region("Worm Pod", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_enter_wormpod(player))
|
||||
for i in world.get_region("Foglast", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_enter_foglast(player))
|
||||
for i in world.get_region("Sage Labyrinth", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_skull_bomb(player))
|
||||
for i in world.get_region("Sage Airship", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_enter_sageship(player))
|
||||
for i in world.get_region("Hylemxylem", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_enter_hylemxylem(player))
|
||||
|
||||
# random start logic (default)
|
||||
if ((not world.random_start[player]) or \
|
||||
(world.random_start[player] and hylics2world.start_location == "Waynehouse")):
|
||||
# entrances
|
||||
for i in world.get_region("Viewax", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player))
|
||||
for i in world.get_region("TV Island", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
for i in world.get_region("Shield Facility", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
for i in world.get_region("Juice Ranch", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
|
||||
# random start logic (Viewax's Edifice)
|
||||
elif (world.random_start[player] and hylics2world.start_location == "Viewax's Edifice"):
|
||||
for i in world.get_region("Waynehouse", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player))
|
||||
for i in world.get_region("New Muldul", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player))
|
||||
for i in world.get_region("New Muldul Vault", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player))
|
||||
for i in world.get_region("Drill Castle", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_can_air_dash(player) or state._hylics2_has_airship(player))
|
||||
for i in world.get_region("TV Island", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
for i in world.get_region("Shield Facility", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
for i in world.get_region("Juice Ranch", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
for i in world.get_region("Sage Labyrinth", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
|
||||
# random start logic (TV Island)
|
||||
elif (world.random_start[player] and hylics2world.start_location == "TV Island"):
|
||||
for i in world.get_region("Waynehouse", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
for i in world.get_region("New Muldul", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
for i in world.get_region("New Muldul Vault", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
for i in world.get_region("Drill Castle", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
for i in world.get_region("Viewax", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
for i in world.get_region("Shield Facility", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
for i in world.get_region("Juice Ranch", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
for i in world.get_region("Sage Labyrinth", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
|
||||
# random start logic (Shield Facility)
|
||||
elif (world.random_start[player] and hylics2world.start_location == "Shield Facility"):
|
||||
for i in world.get_region("Waynehouse", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
for i in world.get_region("New Muldul", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
for i in world.get_region("New Muldul Vault", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
for i in world.get_region("Drill Castle", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
for i in world.get_region("Viewax", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
for i in world.get_region("TV Island", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
for i in world.get_region("Sage Labyrinth", player).entrances:
|
||||
add_rule(i, lambda state: state._hylics2_has_airship(player))
|
||||
@@ -0,0 +1,246 @@
|
||||
import random
|
||||
from typing import Dict, Any
|
||||
from BaseClasses import Region, Entrance, Location, Item, Tutorial, ItemClassification, RegionType
|
||||
from worlds.generic.Rules import set_rule
|
||||
from ..AutoWorld import World, WebWorld
|
||||
from . import Items, Locations, Options, Rules, Exits
|
||||
|
||||
|
||||
class Hylics2Web(WebWorld):
|
||||
theme = "ocean"
|
||||
tutorials = [Tutorial(
|
||||
"Multiworld Setup Guide",
|
||||
"A guide to settings up the Hylics 2 randomizer connected to an Archipelago Multiworld",
|
||||
"English",
|
||||
"setup_en.md",
|
||||
"setup/en",
|
||||
["TRPG"]
|
||||
)]
|
||||
|
||||
|
||||
class Hylics2World(World):
|
||||
"""
|
||||
Hylics 2 is a surreal and unusual RPG, with a bizarre yet unique visual style. Play as Wayne,
|
||||
travel the world, and gather your allies to defeat the nefarious Gibby in his Hylemxylem!
|
||||
"""
|
||||
game: str = "Hylics 2"
|
||||
web = Hylics2Web()
|
||||
|
||||
all_items = {**Items.item_table, **Items.gesture_item_table, **Items.party_item_table,
|
||||
**Items.medallion_item_table}
|
||||
all_locations = {**Locations.location_table, **Locations.tv_location_table, **Locations.party_location_table,
|
||||
**Locations.medallion_location_table}
|
||||
|
||||
item_name_to_id = {data["name"]: item_id for item_id, data in all_items.items()}
|
||||
location_name_to_id = {data["name"]: loc_id for loc_id, data in all_locations.items()}
|
||||
option_definitions = Options.hylics2_options
|
||||
|
||||
topology_present: bool = True
|
||||
remote_items: bool = True
|
||||
remote_start_inventory: bool = True
|
||||
|
||||
data_version: 1
|
||||
|
||||
start_location = "Waynehouse"
|
||||
|
||||
|
||||
def set_rules(self):
|
||||
Rules.set_rules(self)
|
||||
|
||||
|
||||
def create_item(self, name: str) -> "Hylics2Item":
|
||||
item_id: int = self.item_name_to_id[name]
|
||||
|
||||
return Hylics2Item(name, self.all_items[item_id]["classification"], item_id, player=self.player)
|
||||
|
||||
|
||||
def add_item(self, name: str, classification: ItemClassification, code: int) -> "Item":
|
||||
return Hylics2Item(name, classification, code, self.player)
|
||||
|
||||
|
||||
def create_event(self, event: str):
|
||||
return Hylics2Item(event, ItemClassification.progression_skip_balancing, None, self.player)
|
||||
|
||||
|
||||
# set random starting location if option is enabled
|
||||
def generate_early(self):
|
||||
if self.world.random_start[self.player]:
|
||||
i = self.world.random.randint(0, 3)
|
||||
if i == 0:
|
||||
self.start_location = "Waynehouse"
|
||||
elif i == 1:
|
||||
self.start_location = "Viewax's Edifice"
|
||||
elif i == 2:
|
||||
self.start_location = "TV Island"
|
||||
elif i == 3:
|
||||
self.start_location = "Shield Facility"
|
||||
|
||||
def generate_basic(self):
|
||||
# create location for beating the game and place Victory event there
|
||||
loc = Location(self.player, "Defeat Gibby", None, self.world.get_region("Hylemxylem", self.player))
|
||||
loc.place_locked_item(self.create_event("Victory"))
|
||||
set_rule(loc, lambda state: state._hylics2_has_upper_chamber_key(self.player)
|
||||
and state._hylics2_has_vessel_room_key(self.player))
|
||||
self.world.get_region("Hylemxylem", self.player).locations.append(loc)
|
||||
self.world.completion_condition[self.player] = lambda state: state.has("Victory", self.player)
|
||||
|
||||
# create item pool
|
||||
pool = []
|
||||
|
||||
# add regular items
|
||||
for i, data in Items.item_table.items():
|
||||
if data["count"] > 0:
|
||||
for j in range(data["count"]):
|
||||
pool.append(self.add_item(data["name"], data["classification"], i))
|
||||
|
||||
# add party members if option is enabled
|
||||
if self.world.party_shuffle[self.player]:
|
||||
for i, data in Items.party_item_table.items():
|
||||
pool.append(self.add_item(data["name"], data["classification"], i))
|
||||
|
||||
# handle gesture shuffle options
|
||||
if self.world.gesture_shuffle[self.player] == 2: # vanilla locations
|
||||
gestures = Items.gesture_item_table
|
||||
self.world.get_location("Waynehouse: TV", self.player)\
|
||||
.place_locked_item(self.add_item(gestures[200678]["name"], gestures[200678]["classification"], 200678))
|
||||
self.world.get_location("Afterlife: TV", self.player)\
|
||||
.place_locked_item(self.add_item(gestures[200683]["name"], gestures[200683]["classification"], 200683))
|
||||
self.world.get_location("New Muldul: TV", self.player)\
|
||||
.place_locked_item(self.add_item(gestures[200679]["name"], gestures[200679]["classification"], 200679))
|
||||
self.world.get_location("Viewax's Edifice: TV", self.player)\
|
||||
.place_locked_item(self.add_item(gestures[200680]["name"], gestures[200680]["classification"], 200680))
|
||||
self.world.get_location("TV Island: TV", self.player)\
|
||||
.place_locked_item(self.add_item(gestures[200681]["name"], gestures[200681]["classification"], 200681))
|
||||
self.world.get_location("Juice Ranch: TV", self.player)\
|
||||
.place_locked_item(self.add_item(gestures[200682]["name"], gestures[200682]["classification"], 200682))
|
||||
self.world.get_location("Foglast: TV", self.player)\
|
||||
.place_locked_item(self.add_item(gestures[200684]["name"], gestures[200684]["classification"], 200684))
|
||||
self.world.get_location("Drill Castle: TV", self.player)\
|
||||
.place_locked_item(self.add_item(gestures[200688]["name"], gestures[200688]["classification"], 200688))
|
||||
self.world.get_location("Sage Airship: TV", self.player)\
|
||||
.place_locked_item(self.add_item(gestures[200685]["name"], gestures[200685]["classification"], 200685))
|
||||
|
||||
elif self.world.gesture_shuffle[self.player] == 1: # TVs only
|
||||
gestures = list(Items.gesture_item_table.items())
|
||||
tvs = list(Locations.tv_location_table.items())
|
||||
|
||||
# if Extra Items in Logic is enabled place CHARGE UP first and make sure it doesn't get
|
||||
# placed at Sage Airship: TV
|
||||
if self.world.extra_items_in_logic[self.player]:
|
||||
tv = self.world.random.choice(tvs)
|
||||
gest = gestures.index((200681, Items.gesture_item_table[200681]))
|
||||
while tv[1]["name"] == "Sage Airship: TV":
|
||||
tv = self.world.random.choice(tvs)
|
||||
self.world.get_location(tv[1]["name"], self.player)\
|
||||
.place_locked_item(self.add_item(gestures[gest][1]["name"], gestures[gest][1]["classification"],
|
||||
gestures[gest]))
|
||||
gestures.remove(gestures[gest])
|
||||
tvs.remove(tv)
|
||||
|
||||
for i in range(len(gestures)):
|
||||
gest = self.world.random.choice(gestures)
|
||||
tv = self.world.random.choice(tvs)
|
||||
self.world.get_location(tv[1]["name"], self.player)\
|
||||
.place_locked_item(self.add_item(gest[1]["name"], gest[1]["classification"], gest[1]))
|
||||
gestures.remove(gest)
|
||||
tvs.remove(tv)
|
||||
|
||||
else: # add gestures to pool like normal
|
||||
for i, data in Items.gesture_item_table.items():
|
||||
pool.append(self.add_item(data["name"], data["classification"], i))
|
||||
|
||||
# add '10 Bones' items if medallion shuffle is enabled
|
||||
if self.world.medallion_shuffle[self.player]:
|
||||
for i, data in Items.medallion_item_table.items():
|
||||
for j in range(data["count"]):
|
||||
pool.append(self.add_item(data["name"], data["classification"], i))
|
||||
|
||||
# add to world's pool
|
||||
self.world.itempool += pool
|
||||
|
||||
|
||||
def fill_slot_data(self) -> Dict[str, Any]:
|
||||
slot_data: Dict[str, Any] = {
|
||||
"party_shuffle": self.world.party_shuffle[self.player].value,
|
||||
"medallion_shuffle": self.world.medallion_shuffle[self.player].value,
|
||||
"random_start" : self.world.random_start[self.player].value,
|
||||
"start_location" : self.start_location,
|
||||
"death_link": self.world.death_link[self.player].value
|
||||
}
|
||||
return slot_data
|
||||
|
||||
|
||||
def create_regions(self) -> None:
|
||||
|
||||
region_table: Dict[int, Region] = {
|
||||
0: Region("Menu", RegionType.Generic, "Menu", self.player, self.world),
|
||||
1: Region("Afterlife", RegionType.Generic, "Afterlife", self.player, self.world),
|
||||
2: Region("Waynehouse", RegionType.Generic, "Waynehouse", self.player, self.world),
|
||||
3: Region("World", RegionType.Generic, "World", self.player, self.world),
|
||||
4: Region("New Muldul", RegionType.Generic, "New Muldul", self.player, self.world),
|
||||
5: Region("New Muldul Vault", RegionType.Generic, "New Muldul Vault", self.player, self.world),
|
||||
6: Region("Viewax", RegionType.Generic, "Viewax's Edifice", self.player, self.world),
|
||||
7: Region("Airship", RegionType.Generic, "Airship", self.player, self.world),
|
||||
8: Region("Arcade Island", RegionType.Generic, "Arcade Island", self.player, self.world),
|
||||
9: Region("TV Island", RegionType.Generic, "TV Island", self.player, self.world),
|
||||
10: Region("Juice Ranch", RegionType.Generic, "Juice Ranch", self.player, self.world),
|
||||
11: Region("Shield Facility", RegionType.Generic, "Shield Facility", self.player, self.world),
|
||||
12: Region("Worm Pod", RegionType.Generic, "Worm Pod", self.player, self.world),
|
||||
13: Region("Foglast", RegionType.Generic, "Foglast", self.player, self.world),
|
||||
14: Region("Drill Castle", RegionType.Generic, "Drill Castle", self.player, self.world),
|
||||
15: Region("Sage Labyrinth", RegionType.Generic, "Sage Labyrinth", self.player, self.world),
|
||||
16: Region("Sage Airship", RegionType.Generic, "Sage Airship", self.player, self.world),
|
||||
17: Region("Hylemxylem", RegionType.Generic, "Hylemxylem", self.player, self.world)
|
||||
}
|
||||
|
||||
# create regions from table
|
||||
for i, reg in region_table.items():
|
||||
self.world.regions.append(reg)
|
||||
# get all exits per region
|
||||
for j, exits in Exits.region_exit_table.items():
|
||||
if j == i:
|
||||
for k in exits:
|
||||
# create entrance and connect it to parent and destination regions
|
||||
ent = Entrance(self.player, k, reg)
|
||||
reg.exits.append(ent)
|
||||
if k == "New Game" and self.world.random_start[self.player]:
|
||||
if self.start_location == "Waynehouse":
|
||||
ent.connect(region_table[2])
|
||||
elif self.start_location == "Viewax's Edifice":
|
||||
ent.connect(region_table[6])
|
||||
elif self.start_location == "TV Island":
|
||||
ent.connect(region_table[9])
|
||||
elif self.start_location == "Shield Facility":
|
||||
ent.connect(region_table[11])
|
||||
else:
|
||||
for name, num in Exits.exit_lookup_table.items():
|
||||
if k == name:
|
||||
ent.connect(region_table[num])
|
||||
|
||||
# add regular locations
|
||||
for i, data in Locations.location_table.items():
|
||||
region_table[data["region"]].locations\
|
||||
.append(Hylics2Location(self.player, data["name"], i, region_table[data["region"]]))
|
||||
for i, data in Locations.tv_location_table.items():
|
||||
region_table[data["region"]].locations\
|
||||
.append(Hylics2Location(self.player, data["name"], i, region_table[data["region"]]))
|
||||
|
||||
# add party member locations if option is enabled
|
||||
if self.world.party_shuffle[self.player]:
|
||||
for i, data in Locations.party_location_table.items():
|
||||
region_table[data["region"]].locations\
|
||||
.append(Hylics2Location(self.player, data["name"], i, region_table[data["region"]]))
|
||||
|
||||
# add medallion locations if option is enabled
|
||||
if self.world.medallion_shuffle[self.player]:
|
||||
for i, data in Locations.medallion_location_table.items():
|
||||
region_table[data["region"]].locations\
|
||||
.append(Hylics2Location(self.player, data["name"], i, region_table[data["region"]]))
|
||||
|
||||
|
||||
class Hylics2Location(Location):
|
||||
game: str = "Hylics 2"
|
||||
|
||||
|
||||
class Hylics2Item(Item):
|
||||
game: str = "Hylics 2"
|
||||
@@ -0,0 +1,17 @@
|
||||
# Hylics 2
|
||||
|
||||
## Where is the settings page?
|
||||
|
||||
The [player settings page for this game](../player-settings) contains all the options you need to configure and export a config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
|
||||
In Hylics 2, all unique items, equipment and skills are randomized. This includes items in chests, items that are freely standing in the world, items recieved from talking to certain characters, gestures learned from TVs, and so on. Items recieved from completing battles are not randomized, with the exception of the Jail Key recieved from defeating Viewax.
|
||||
|
||||
## What Hylics 2 items can appear in other players' worlds?
|
||||
|
||||
Consumable items, key items, gloves, accessories, and gestures can appear in other players' worlds.
|
||||
|
||||
## What does another world's item look like in Hylics 2?
|
||||
|
||||
All items retain their original appearance. You won't know if an item belongs to another player until you collect it.
|
||||
@@ -0,0 +1,35 @@
|
||||
# Hylics 2 Randomizer Setup Guide
|
||||
|
||||
## Required Software
|
||||
|
||||
- Hylics 2 from: [Steam](https://store.steampowered.com/app/1286710/Hylics_2/) or [itch.io](https://mason-lindroth.itch.io/hylics-2)
|
||||
- BepInEx from: [GitHub](https://github.com/BepInEx/BepInEx/releases)
|
||||
- Archipelago Mod for Hylics 2 from: [GitHub](https://github.com/TRPG0/ArchipelagoHylics2)
|
||||
|
||||
## Instructions (Windows)
|
||||
|
||||
1. Download and install BepInEx 5 (32-bit, version 5.4.20 or newer) to your Hylics 2 root folder. *Do not use any pre-release versions of BepInEx 6.*
|
||||
|
||||
2. Start Hylics 2 once so that BepInEx can create its required configuration files.
|
||||
|
||||
3. Download the latest version of ArchipelagoHylics2 from the [Releases](https://github.com/TRPG0/ArchipelagoHylics2/releases) page and extract the contents of the zip file into `BepInEx\plugins`.
|
||||
|
||||
4. Start Hylics 2 again. To verify that the mod is working, begin a new game or load a save file.
|
||||
|
||||
## Connecting
|
||||
|
||||
To connect to an Archipelago server, open the in-game console (default key: `/`) and use the command `/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 that you have connected to a server at least once before attempting to check any locations.**
|
||||
|
||||
## Other Commands
|
||||
|
||||
There are a few additional commands that can be used while playing Hylics 2 randomizer:
|
||||
|
||||
- `/disconnect` - Disconnect from an Archipelago server.
|
||||
- `/popups` - Enables or disables in-game messages when an item is found or recieved.
|
||||
- `/airship` - Resummons the airship at the dock above New Muldul and teleports Wayne to it, in case the player gets stuck. Player must have the DOCK KEY to use this command.
|
||||
- `/respawn` - Moves Wayne back to the spawn position of the current area in case you get stuck. `/respawn home` will teleport Wayne back to his original starting position.
|
||||
- `/checked [region]` - States how many locations have been checked in a given region. If no region is given, then the player's location will be used.
|
||||
- `/deathlink` - Enables or disables DeathLink.
|
||||
- `/help [command]` - Lists a command, it's description, and it's required arguments (if any). If no command is given, all commands will be displayed.
|
||||
- `![command]` - Entering any command with an `!` at the beginning allows for remotely sending commands to the server.
|
||||
@@ -173,7 +173,7 @@ def set_advancement_rules(world: MultiWorld, player: int):
|
||||
state.can_reach("Hero of the Village", "Location", player)) # Bad Omen, Hero of the Village
|
||||
set_rule(world.get_location("Bullseye", player), lambda state: state.has("Archery", player) and state.has("Progressive Tools", player, 2) and state._mc_has_iron_ingots(player))
|
||||
set_rule(world.get_location("Spooky Scary Skeleton", player), lambda state: state._mc_basic_combat(player))
|
||||
set_rule(world.get_location("Two by Two", player), lambda state: state._mc_has_iron_ingots(player) and state._mc_can_adventure(player)) # shears > seagrass > turtles; nether > striders; gold carrots > horses skips ingots
|
||||
set_rule(world.get_location("Two by Two", player), lambda state: state._mc_has_iron_ingots(player) and state.has("Bucket", player) and state._mc_can_adventure(player)) # shears > seagrass > turtles; buckets of tropical fish > axolotls; nether > striders; gold carrots > horses skips ingots
|
||||
# set_rule(world.get_location("Stone Age", player), lambda state: True)
|
||||
set_rule(world.get_location("Two Birds, One Arrow", player), lambda state: state._mc_craft_crossbow(player) and state._mc_can_enchant(player))
|
||||
# set_rule(world.get_location("We Need to Go Deeper", player), lambda state: True)
|
||||
|
||||
@@ -150,7 +150,7 @@ class MinecraftWorld(World):
|
||||
|
||||
def generate_output(self, output_directory: str):
|
||||
data = self._get_mc_data()
|
||||
filename = f"AP_{self.world.seed_name}_P{self.player}_{self.world.get_file_safe_player_name(self.player)}.apmc"
|
||||
filename = f"{self.world.get_out_file_name_base(self.player)}.apmc"
|
||||
with open(os.path.join(output_directory, filename), 'wb') as f:
|
||||
f.write(b64encode(bytes(json.dumps(data), 'utf-8')))
|
||||
|
||||
|
||||
@@ -343,6 +343,27 @@ priority_entrance_table = {
|
||||
}
|
||||
|
||||
|
||||
# These hint texts have more than one entrance, so they are OK for impa's house and potion shop
|
||||
multi_interior_regions = {
|
||||
'Kokiri Forest',
|
||||
'Lake Hylia',
|
||||
'the Market',
|
||||
'Kakariko Village',
|
||||
'Lon Lon Ranch',
|
||||
}
|
||||
|
||||
interior_entrance_bias = {
|
||||
'Kakariko Village -> Kak Potion Shop Front': 4,
|
||||
'Kak Backyard -> Kak Potion Shop Back': 4,
|
||||
'Kakariko Village -> Kak Impas House': 3,
|
||||
'Kak Impas Ledge -> Kak Impas House Back': 3,
|
||||
'Goron City -> GC Shop': 2,
|
||||
'Zoras Domain -> ZD Shop': 2,
|
||||
'Market Entrance -> Market Guard House': 2,
|
||||
'ToT Entrance -> Temple of Time': 1,
|
||||
}
|
||||
|
||||
|
||||
class EntranceShuffleError(Exception):
|
||||
pass
|
||||
|
||||
@@ -500,7 +521,7 @@ def shuffle_random_entrances(ootworld):
|
||||
delete_target_entrance(remaining_target)
|
||||
|
||||
for pool_type, entrance_pool in one_way_entrance_pools.items():
|
||||
shuffle_entrance_pool(ootworld, entrance_pool, one_way_target_entrance_pools[pool_type], locations_to_ensure_reachable, all_state, none_state, check_all=True, retry_count=5)
|
||||
shuffle_entrance_pool(ootworld, pool_type, entrance_pool, one_way_target_entrance_pools[pool_type], locations_to_ensure_reachable, all_state, none_state, check_all=True, retry_count=5)
|
||||
replaced_entrances = [entrance.replaces for entrance in entrance_pool]
|
||||
for remaining_target in chain.from_iterable(one_way_target_entrance_pools.values()):
|
||||
if remaining_target.replaces in replaced_entrances:
|
||||
@@ -510,7 +531,7 @@ def shuffle_random_entrances(ootworld):
|
||||
|
||||
# Shuffle all entrance pools, in order
|
||||
for pool_type, entrance_pool in entrance_pools.items():
|
||||
shuffle_entrance_pool(ootworld, entrance_pool, target_entrance_pools[pool_type], locations_to_ensure_reachable, all_state, none_state)
|
||||
shuffle_entrance_pool(ootworld, pool_type, entrance_pool, target_entrance_pools[pool_type], locations_to_ensure_reachable, all_state, none_state, check_all=True)
|
||||
|
||||
# Multiple checks after shuffling to ensure everything is OK
|
||||
# Check that all entrances hook up correctly
|
||||
@@ -596,7 +617,7 @@ def place_one_way_priority_entrance(ootworld, priority_name, allowed_regions, al
|
||||
raise EntranceShuffleError(f'Unable to place priority one-way entrance for {priority_name} in world {ootworld.player}')
|
||||
|
||||
|
||||
def shuffle_entrance_pool(ootworld, entrance_pool, target_entrances, locations_to_ensure_reachable, all_state, none_state, check_all=False, retry_count=20):
|
||||
def shuffle_entrance_pool(ootworld, pool_type, entrance_pool, target_entrances, locations_to_ensure_reachable, all_state, none_state, check_all=False, retry_count=20):
|
||||
|
||||
restrictive_entrances, soft_entrances = split_entrances_by_requirements(ootworld, entrance_pool, target_entrances)
|
||||
|
||||
@@ -604,11 +625,11 @@ def shuffle_entrance_pool(ootworld, entrance_pool, target_entrances, locations_t
|
||||
retry_count -= 1
|
||||
rollbacks = []
|
||||
try:
|
||||
shuffle_entrances(ootworld, restrictive_entrances, target_entrances, rollbacks, locations_to_ensure_reachable, all_state, none_state)
|
||||
shuffle_entrances(ootworld, pool_type+'Rest', restrictive_entrances, target_entrances, rollbacks, locations_to_ensure_reachable, all_state, none_state)
|
||||
if check_all:
|
||||
shuffle_entrances(ootworld, soft_entrances, target_entrances, rollbacks, locations_to_ensure_reachable, all_state, none_state)
|
||||
shuffle_entrances(ootworld, pool_type+'Soft', soft_entrances, target_entrances, rollbacks, locations_to_ensure_reachable, all_state, none_state)
|
||||
else:
|
||||
shuffle_entrances(ootworld, soft_entrances, target_entrances, rollbacks, set(), all_state, none_state)
|
||||
shuffle_entrances(ootworld, pool_type+'Soft', soft_entrances, target_entrances, rollbacks, set(), all_state, none_state)
|
||||
|
||||
validate_world(ootworld, None, locations_to_ensure_reachable, all_state, none_state)
|
||||
for entrance, target in rollbacks:
|
||||
@@ -621,12 +642,16 @@ def shuffle_entrance_pool(ootworld, entrance_pool, target_entrances, locations_t
|
||||
|
||||
raise EntranceShuffleError(f'Entrance placement attempt count exceeded for world {ootworld.player}')
|
||||
|
||||
def shuffle_entrances(ootworld, entrances, target_entrances, rollbacks, locations_to_ensure_reachable, all_state, none_state):
|
||||
def shuffle_entrances(ootworld, pool_type, entrances, target_entrances, rollbacks, locations_to_ensure_reachable, all_state, none_state):
|
||||
ootworld.world.random.shuffle(entrances)
|
||||
for entrance in entrances:
|
||||
if entrance.connected_region != None:
|
||||
continue
|
||||
ootworld.world.random.shuffle(target_entrances)
|
||||
# Here we deliberately introduce bias by prioritizing certain interiors, i.e. the ones most likely to cause problems.
|
||||
# success rate over randomization
|
||||
if pool_type in {'InteriorSoft', 'MixedSoft'}:
|
||||
target_entrances.sort(reverse=True, key=lambda entrance: interior_entrance_bias.get(entrance.replaces.name, 0))
|
||||
for target in target_entrances:
|
||||
if target.connected_region == None:
|
||||
continue
|
||||
@@ -715,25 +740,33 @@ def validate_world(ootworld, entrance_placed, locations_to_ensure_reachable, all
|
||||
|
||||
# Check if all locations are reachable if not beatable-only or game is not yet complete
|
||||
if locations_to_ensure_reachable:
|
||||
if world.accessibility[player].current_key != 'minimal' or not world.can_beat_game(all_state):
|
||||
for loc in locations_to_ensure_reachable:
|
||||
if not all_state.can_reach(loc, 'Location', player):
|
||||
raise EntranceShuffleError(f'{loc} is unreachable')
|
||||
for loc in locations_to_ensure_reachable:
|
||||
if not all_state.can_reach(loc, 'Location', player):
|
||||
raise EntranceShuffleError(f'{loc} is unreachable')
|
||||
|
||||
if ootworld.shuffle_interior_entrances and (ootworld.misc_hints or ootworld.hints != 'none') and \
|
||||
(entrance_placed == None or entrance_placed.type in ['Interior', 'SpecialInterior']):
|
||||
# Ensure Kak Potion Shop entrances are in the same hint area so there is no ambiguity as to which entrance is used for hints
|
||||
potion_front_entrance = get_entrance_replacing(world.get_region('Kak Potion Shop Front', player), 'Kakariko Village -> Kak Potion Shop Front', player)
|
||||
potion_back_entrance = get_entrance_replacing(world.get_region('Kak Potion Shop Back', player), 'Kak Backyard -> Kak Potion Shop Back', player)
|
||||
if potion_front_entrance is not None and potion_back_entrance is not None and not same_hint_area(potion_front_entrance, potion_back_entrance):
|
||||
potion_front = get_entrance_replacing(world.get_region('Kak Potion Shop Front', player), 'Kakariko Village -> Kak Potion Shop Front', player)
|
||||
potion_back = get_entrance_replacing(world.get_region('Kak Potion Shop Back', player), 'Kak Backyard -> Kak Potion Shop Back', player)
|
||||
if potion_front is not None and potion_back is not None and not same_hint_area(potion_front, potion_back):
|
||||
raise EntranceShuffleError('Kak Potion Shop entrances are not in the same hint area')
|
||||
elif (potion_front and not potion_back) or (not potion_front and potion_back):
|
||||
# Check the hint area and ensure it's one of the ones with more than one entrance
|
||||
potion_placed_entrance = potion_front if potion_front else potion_back
|
||||
if get_hint_area(potion_placed_entrance) not in multi_interior_regions:
|
||||
raise EntranceShuffleError('Kak Potion Shop entrances can never be in the same hint area')
|
||||
|
||||
# When cows are shuffled, ensure the same thing for Impa's House, since the cow is reachable from both sides
|
||||
if ootworld.shuffle_cows:
|
||||
impas_front_entrance = get_entrance_replacing(world.get_region('Kak Impas House', player), 'Kakariko Village -> Kak Impas House', player)
|
||||
impas_back_entrance = get_entrance_replacing(world.get_region('Kak Impas House Back', player), 'Kak Impas Ledge -> Kak Impas House Back', player)
|
||||
if impas_front_entrance is not None and impas_back_entrance is not None and not same_hint_area(impas_front_entrance, impas_back_entrance):
|
||||
impas_front = get_entrance_replacing(world.get_region('Kak Impas House', player), 'Kakariko Village -> Kak Impas House', player)
|
||||
impas_back = get_entrance_replacing(world.get_region('Kak Impas House Back', player), 'Kak Impas Ledge -> Kak Impas House Back', player)
|
||||
if impas_front is not None and impas_back is not None and not same_hint_area(impas_front, impas_back):
|
||||
raise EntranceShuffleError('Kak Impas House entrances are not in the same hint area')
|
||||
elif (impas_front and not impas_back) or (not impas_front and impas_back):
|
||||
impas_placed_entrance = impas_front if impas_front else impas_back
|
||||
if get_hint_area(impas_placed_entrance) not in multi_interior_regions:
|
||||
raise EntranceShuffleError('Kak Impas House entrances can never be in the same hint area')
|
||||
|
||||
# Check basic refills, time passing, return to ToT
|
||||
if (ootworld.shuffle_special_interior_entrances or ootworld.shuffle_overworld_entrances or ootworld.spawn_positions) and \
|
||||
@@ -845,3 +878,4 @@ def delete_target_entrance(target):
|
||||
if target.parent_region != None:
|
||||
target.parent_region.exits.remove(target)
|
||||
target.parent_region = None
|
||||
del target
|
||||
|
||||
+57
-51
@@ -1,7 +1,7 @@
|
||||
import logging
|
||||
import threading
|
||||
import copy
|
||||
from collections import Counter
|
||||
from collections import Counter, deque
|
||||
|
||||
logger = logging.getLogger("Ocarina of Time")
|
||||
|
||||
@@ -412,17 +412,6 @@ class OOTWorld(World):
|
||||
self.shop_prices[location.name] = int(self.world.random.betavariate(1.5, 2) * 60) * 5
|
||||
|
||||
def fill_bosses(self, bossCount=9):
|
||||
rewardlist = (
|
||||
'Kokiri Emerald',
|
||||
'Goron Ruby',
|
||||
'Zora Sapphire',
|
||||
'Forest Medallion',
|
||||
'Fire Medallion',
|
||||
'Water Medallion',
|
||||
'Spirit Medallion',
|
||||
'Shadow Medallion',
|
||||
'Light Medallion'
|
||||
)
|
||||
boss_location_names = (
|
||||
'Queen Gohma',
|
||||
'King Dodongo',
|
||||
@@ -434,7 +423,7 @@ class OOTWorld(World):
|
||||
'Twinrova',
|
||||
'Links Pocket'
|
||||
)
|
||||
boss_rewards = [self.create_item(reward) for reward in rewardlist]
|
||||
boss_rewards = [item for item in self.itempool if item.type == 'DungeonReward']
|
||||
boss_locations = [self.world.get_location(loc, self.player) for loc in boss_location_names]
|
||||
|
||||
placed_prizes = [loc.item.name for loc in boss_locations if loc.item is not None]
|
||||
@@ -447,9 +436,8 @@ class OOTWorld(World):
|
||||
self.world.random.shuffle(prize_locs)
|
||||
item = prizepool.pop()
|
||||
loc = prize_locs.pop()
|
||||
self.world.push_item(loc, item, collect=False)
|
||||
loc.locked = True
|
||||
loc.event = True
|
||||
loc.place_locked_item(item)
|
||||
self.world.itempool.remove(item)
|
||||
|
||||
def create_item(self, name: str):
|
||||
if name in item_table:
|
||||
@@ -496,6 +484,10 @@ class OOTWorld(World):
|
||||
# Generate itempool
|
||||
generate_itempool(self)
|
||||
add_dungeon_items(self)
|
||||
# Add dungeon rewards
|
||||
rewardlist = sorted(list(self.item_name_groups['rewards']))
|
||||
self.itempool += map(self.create_item, rewardlist)
|
||||
|
||||
junk_pool = get_junk_pool(self)
|
||||
removed_items = []
|
||||
# Determine starting items
|
||||
@@ -621,61 +613,64 @@ class OOTWorld(World):
|
||||
"Gerudo Training Ground Maze Path Final Chest", "Gerudo Training Ground MQ Ice Arrows Chest",
|
||||
]
|
||||
|
||||
def get_names(items):
|
||||
for item in items:
|
||||
yield item.name
|
||||
|
||||
# Place/set rules for dungeon items
|
||||
itempools = {
|
||||
'dungeon': [],
|
||||
'overworld': [],
|
||||
'any_dungeon': [],
|
||||
'dungeon': set(),
|
||||
'overworld': set(),
|
||||
'any_dungeon': set(),
|
||||
}
|
||||
any_dungeon_locations = []
|
||||
for dungeon in self.dungeons:
|
||||
itempools['dungeon'] = []
|
||||
itempools['dungeon'] = set()
|
||||
# Put the dungeon items into their appropriate pools.
|
||||
# Build in reverse order since we need to fill boss key first and pop() returns the last element
|
||||
if self.shuffle_mapcompass in itempools:
|
||||
itempools[self.shuffle_mapcompass].extend(dungeon.dungeon_items)
|
||||
itempools[self.shuffle_mapcompass].update(get_names(dungeon.dungeon_items))
|
||||
if self.shuffle_smallkeys in itempools:
|
||||
itempools[self.shuffle_smallkeys].extend(dungeon.small_keys)
|
||||
itempools[self.shuffle_smallkeys].update(get_names(dungeon.small_keys))
|
||||
shufflebk = self.shuffle_bosskeys if dungeon.name != 'Ganons Castle' else self.shuffle_ganon_bosskey
|
||||
if shufflebk in itempools:
|
||||
itempools[shufflebk].extend(dungeon.boss_key)
|
||||
itempools[shufflebk].update(get_names(dungeon.boss_key))
|
||||
|
||||
# We can't put a dungeon item on the end of a dungeon if a song is supposed to go there. Make sure not to include it.
|
||||
dungeon_locations = [loc for region in dungeon.regions for loc in region.locations
|
||||
if loc.item is None and (
|
||||
self.shuffle_song_items != 'dungeon' or loc.name not in dungeon_song_locations)]
|
||||
if itempools['dungeon']: # only do this if there's anything to shuffle
|
||||
for item in itempools['dungeon']:
|
||||
dungeon_itempool = [item for item in self.world.itempool if item.player == self.player and item.name in itempools['dungeon']]
|
||||
for item in dungeon_itempool:
|
||||
self.world.itempool.remove(item)
|
||||
self.world.random.shuffle(dungeon_locations)
|
||||
fill_restrictive(self.world, self.world.get_all_state(False), dungeon_locations,
|
||||
itempools['dungeon'], True, True)
|
||||
dungeon_itempool, True, True)
|
||||
any_dungeon_locations.extend(dungeon_locations) # adds only the unfilled locations
|
||||
|
||||
# Now fill items that can go into any dungeon. Retrieve the Gerudo Fortress keys from the pool if necessary
|
||||
if self.shuffle_fortresskeys == 'any_dungeon':
|
||||
fortresskeys = filter(lambda item: item.player == self.player and item.type == 'HideoutSmallKey',
|
||||
self.world.itempool)
|
||||
itempools['any_dungeon'].extend(fortresskeys)
|
||||
itempools['any_dungeon'].add('Small Key (Thieves Hideout)')
|
||||
if itempools['any_dungeon']:
|
||||
for item in itempools['any_dungeon']:
|
||||
any_dungeon_itempool = [item for item in self.world.itempool if item.player == self.player and item.name in itempools['any_dungeon']]
|
||||
for item in any_dungeon_itempool:
|
||||
self.world.itempool.remove(item)
|
||||
itempools['any_dungeon'].sort(key=lambda item:
|
||||
{'GanonBossKey': 4, 'BossKey': 3, 'SmallKey': 2, 'HideoutSmallKey': 1}.get(item.type, 0))
|
||||
any_dungeon_itempool.sort(key=lambda item:
|
||||
{'GanonBossKey': 4, 'BossKey': 3, 'SmallKey': 2, 'HideoutSmallKey': 1}.get(item.type, 0))
|
||||
self.world.random.shuffle(any_dungeon_locations)
|
||||
fill_restrictive(self.world, self.world.get_all_state(False), any_dungeon_locations,
|
||||
itempools['any_dungeon'], True, True)
|
||||
any_dungeon_itempool, True, True)
|
||||
|
||||
# If anything is overworld-only, fill into local non-dungeon locations
|
||||
if self.shuffle_fortresskeys == 'overworld':
|
||||
fortresskeys = filter(lambda item: item.player == self.player and item.type == 'HideoutSmallKey',
|
||||
self.world.itempool)
|
||||
itempools['overworld'].extend(fortresskeys)
|
||||
itempools['overworld'].add('Small Key (Thieves Hideout)')
|
||||
if itempools['overworld']:
|
||||
for item in itempools['overworld']:
|
||||
overworld_itempool = [item for item in self.world.itempool if item.player == self.player and item.name in itempools['overworld']]
|
||||
for item in overworld_itempool:
|
||||
self.world.itempool.remove(item)
|
||||
itempools['overworld'].sort(key=lambda item:
|
||||
{'GanonBossKey': 4, 'BossKey': 3, 'SmallKey': 2, 'HideoutSmallKey': 1}.get(item.type, 0))
|
||||
overworld_itempool.sort(key=lambda item:
|
||||
{'GanonBossKey': 4, 'BossKey': 3, 'SmallKey': 2, 'HideoutSmallKey': 1}.get(item.type, 0))
|
||||
non_dungeon_locations = [loc for loc in self.get_locations() if
|
||||
not loc.item and loc not in any_dungeon_locations and
|
||||
(loc.type != 'Shop' or loc.name in self.shop_prices) and
|
||||
@@ -683,7 +678,7 @@ class OOTWorld(World):
|
||||
(loc.name not in dungeon_song_locations or self.shuffle_song_items != 'dungeon')]
|
||||
self.world.random.shuffle(non_dungeon_locations)
|
||||
fill_restrictive(self.world, self.world.get_all_state(False), non_dungeon_locations,
|
||||
itempools['overworld'], True, True)
|
||||
overworld_itempool, True, True)
|
||||
|
||||
# Place songs
|
||||
# 5 built-in retries because this section can fail sometimes
|
||||
@@ -805,6 +800,10 @@ class OOTWorld(World):
|
||||
or (self.skip_child_zelda and loc.name in ['HC Zeldas Letter', 'Song from Impa'])):
|
||||
loc.address = None
|
||||
|
||||
# Handle item-linked dungeon items and songs
|
||||
def stage_pre_fill(cls):
|
||||
pass
|
||||
|
||||
def generate_output(self, output_directory: str):
|
||||
if self.hints != 'none':
|
||||
self.hint_data_available.wait()
|
||||
@@ -819,7 +818,7 @@ class OOTWorld(World):
|
||||
# Seed hint RNG, used for ganon text lines also
|
||||
self.hint_rng = self.world.slot_seeds[self.player]
|
||||
|
||||
outfile_name = f"AP_{self.world.seed_name}_P{self.player}_{self.world.get_file_safe_player_name(self.player)}"
|
||||
outfile_name = self.world.get_out_file_name_base(self.player)
|
||||
rom = Rom(file=get_options()['oot_options']['rom_file'])
|
||||
if self.hints != 'none':
|
||||
buildWorldGossipHints(self)
|
||||
@@ -831,18 +830,25 @@ class OOTWorld(World):
|
||||
|
||||
# Write entrances to spoiler log
|
||||
all_entrances = self.get_shuffled_entrances()
|
||||
all_entrances.sort(key=lambda x: x.name)
|
||||
all_entrances.sort(key=lambda x: x.type)
|
||||
all_entrances.sort(reverse=True, key=lambda x: x.name)
|
||||
all_entrances.sort(reverse=True, key=lambda x: x.type)
|
||||
if not self.decouple_entrances:
|
||||
for loadzone in all_entrances:
|
||||
if loadzone.primary:
|
||||
entrance = loadzone
|
||||
while all_entrances:
|
||||
loadzone = all_entrances.pop()
|
||||
if loadzone.type != 'Overworld':
|
||||
if loadzone.primary:
|
||||
entrance = loadzone
|
||||
else:
|
||||
entrance = loadzone.reverse
|
||||
if entrance.reverse is not None:
|
||||
self.world.spoiler.set_entrance(entrance, entrance.replaces.reverse, 'both', self.player)
|
||||
else:
|
||||
self.world.spoiler.set_entrance(entrance, entrance.replaces, 'entrance', self.player)
|
||||
else:
|
||||
entrance = loadzone.reverse
|
||||
if entrance.reverse is not None:
|
||||
self.world.spoiler.set_entrance(entrance, entrance.replaces.reverse, 'both', self.player)
|
||||
else:
|
||||
self.world.spoiler.set_entrance(entrance, entrance.replaces, 'entrance', self.player)
|
||||
reverse = loadzone.replaces.reverse
|
||||
if reverse in all_entrances:
|
||||
all_entrances.remove(reverse)
|
||||
self.world.spoiler.set_entrance(loadzone, reverse, 'both', self.player)
|
||||
else:
|
||||
for entrance in all_entrances:
|
||||
self.world.spoiler.set_entrance(entrance, entrance.replaces, 'entrance', self.player)
|
||||
@@ -1027,7 +1033,7 @@ class OOTWorld(World):
|
||||
all_state = self.world.get_all_state(use_cache=False)
|
||||
# Remove event progression items
|
||||
for item, player in all_state.prog_items:
|
||||
if (item not in item_table or item_table[item][2] is None) and player == self.player:
|
||||
if player == self.player and (item not in item_table or (item_table[item][2] is None and item_table[item][0] != 'DungeonReward')):
|
||||
all_state.prog_items[(item, player)] = 0
|
||||
# Remove all events and checked locations
|
||||
all_state.locations_checked = {loc for loc in all_state.locations_checked if loc.player != self.player}
|
||||
|
||||
@@ -62,7 +62,7 @@ class OriBlindForest(World):
|
||||
|
||||
def generate_basic(self):
|
||||
for item_name, count in default_pool.items():
|
||||
self.world.itempool.extend([self.create_item(item_name)] * count)
|
||||
self.world.itempool.extend([self.create_item(item_name) for _ in range(count)])
|
||||
|
||||
def create_item(self, name: str) -> Item:
|
||||
return Item(name,
|
||||
|
||||
@@ -0,0 +1,152 @@
|
||||
from BaseClasses import Item
|
||||
from typing import NamedTuple, Dict
|
||||
|
||||
|
||||
class ItemData(NamedTuple):
|
||||
code: int
|
||||
|
||||
|
||||
class Overcooked2Item(Item):
|
||||
game: str = "Overcooked! 2"
|
||||
|
||||
|
||||
oc2_base_id = 213700
|
||||
|
||||
item_table: Dict[str, ItemData] = {
|
||||
"Wood" : ItemData(oc2_base_id + 1 ),
|
||||
"Coal Bucket" : ItemData(oc2_base_id + 2 ),
|
||||
"Spare Plate" : ItemData(oc2_base_id + 3 ),
|
||||
"Fire Extinguisher" : ItemData(oc2_base_id + 4 ),
|
||||
"Bellows" : ItemData(oc2_base_id + 5 ),
|
||||
"Clean Dishes" : ItemData(oc2_base_id + 6 ),
|
||||
"Larger Tip Jar" : ItemData(oc2_base_id + 7 ),
|
||||
"Progressive Dash" : ItemData(oc2_base_id + 8 ),
|
||||
"Progressive Throw/Catch" : ItemData(oc2_base_id + 9 ),
|
||||
"Coin Purse" : ItemData(oc2_base_id + 10),
|
||||
"Control Stick Batteries" : ItemData(oc2_base_id + 11),
|
||||
"Wok Wheels" : ItemData(oc2_base_id + 12),
|
||||
"Dish Scrubber" : ItemData(oc2_base_id + 13),
|
||||
"Burn Leniency" : ItemData(oc2_base_id + 14),
|
||||
"Sharp Knife" : ItemData(oc2_base_id + 15),
|
||||
"Order Lookahead" : ItemData(oc2_base_id + 16),
|
||||
"Lightweight Backpack" : ItemData(oc2_base_id + 17),
|
||||
"Faster Respawn Time" : ItemData(oc2_base_id + 18),
|
||||
"Faster Condiment/Drink Switch" : ItemData(oc2_base_id + 19),
|
||||
"Guest Patience" : ItemData(oc2_base_id + 20),
|
||||
"Kevin-1" : ItemData(oc2_base_id + 21),
|
||||
"Kevin-2" : ItemData(oc2_base_id + 22),
|
||||
"Kevin-3" : ItemData(oc2_base_id + 23),
|
||||
"Kevin-4" : ItemData(oc2_base_id + 24),
|
||||
"Kevin-5" : ItemData(oc2_base_id + 25),
|
||||
"Kevin-6" : ItemData(oc2_base_id + 26),
|
||||
"Kevin-7" : ItemData(oc2_base_id + 27),
|
||||
"Kevin-8" : ItemData(oc2_base_id + 28),
|
||||
"Cooking Emote" : ItemData(oc2_base_id + 29),
|
||||
"Curse Emote" : ItemData(oc2_base_id + 30),
|
||||
"Serving Emote" : ItemData(oc2_base_id + 31),
|
||||
"Preparing Emote" : ItemData(oc2_base_id + 32),
|
||||
"Washing Up Emote" : ItemData(oc2_base_id + 33),
|
||||
"Ok Emote" : ItemData(oc2_base_id + 34),
|
||||
"Ramp Button" : ItemData(oc2_base_id + 35),
|
||||
"Bonus Star" : ItemData(oc2_base_id + 36),
|
||||
"Calmer Unbread" : ItemData(oc2_base_id + 37),
|
||||
}
|
||||
|
||||
item_frequencies = {
|
||||
"Progressive Throw/Catch": 2,
|
||||
"Larger Tip Jar": 2,
|
||||
"Order Lookahead": 2,
|
||||
"Progressive Dash": 2,
|
||||
"Bonus Star": 0, # Filler Item
|
||||
# default: 1
|
||||
}
|
||||
|
||||
item_name_to_config_name = {
|
||||
"Wood" : "DisableWood" ,
|
||||
"Coal Bucket" : "DisableCoal" ,
|
||||
"Spare Plate" : "DisableOnePlate" ,
|
||||
"Fire Extinguisher" : "DisableFireExtinguisher" ,
|
||||
"Bellows" : "DisableBellows" ,
|
||||
"Clean Dishes" : "PlatesStartDirty" ,
|
||||
"Control Stick Batteries" : "DisableControlStick" ,
|
||||
"Wok Wheels" : "DisableWokDrag" ,
|
||||
"Dish Scrubber" : "WashTimeMultiplier" ,
|
||||
"Burn Leniency" : "BurnSpeedMultiplier" ,
|
||||
"Sharp Knife" : "ChoppingTimeScale" ,
|
||||
"Lightweight Backpack" : "BackpackMovementScale" ,
|
||||
"Faster Respawn Time" : "RespawnTime" ,
|
||||
"Faster Condiment/Drink Switch": "CarnivalDispenserRefactoryTime",
|
||||
"Guest Patience" : "CustomOrderLifetime" ,
|
||||
"Ramp Button" : "DisableRampButton" ,
|
||||
"Calmer Unbread" : "AggressiveHorde" ,
|
||||
"Coin Purse" : "DisableEarnHordeMoney" ,
|
||||
}
|
||||
|
||||
vanilla_values = {
|
||||
"DisableWood": False,
|
||||
"DisableCoal": False,
|
||||
"DisableOnePlate": False,
|
||||
"DisableFireExtinguisher": False,
|
||||
"DisableBellows": False,
|
||||
"PlatesStartDirty": False,
|
||||
"DisableControlStick": False,
|
||||
"DisableWokDrag": False,
|
||||
"DisableRampButton": False,
|
||||
"WashTimeMultiplier": 1.0,
|
||||
"BurnSpeedMultiplier": 1.0,
|
||||
"ChoppingTimeScale": 1.0,
|
||||
"BackpackMovementScale": 1.0,
|
||||
"RespawnTime": 5.0,
|
||||
"CarnivalDispenserRefactoryTime": 0.0,
|
||||
"CustomOrderLifetime": 100.0,
|
||||
"AggressiveHorde": False,
|
||||
"DisableEarnHordeMoney": False,
|
||||
}
|
||||
|
||||
item_id_to_name: Dict[int, str] = {
|
||||
data.code: item_name for item_name, data in item_table.items() if data.code
|
||||
}
|
||||
|
||||
item_name_to_id: Dict[str, int] = {
|
||||
item_name: data.code for item_name, data in item_table.items() if data.code
|
||||
}
|
||||
|
||||
|
||||
def is_progression(item_name: str) -> bool:
|
||||
return not item_name.endswith("Emote")
|
||||
|
||||
|
||||
def item_to_unlock_event(item_name: str) -> Dict[str, str]:
|
||||
message = f"{item_name} Acquired!"
|
||||
action = ""
|
||||
payload = ""
|
||||
if item_name.startswith("Kevin"):
|
||||
kevin_num = int(item_name.split("-")[-1])
|
||||
action = "UNLOCK_LEVEL"
|
||||
payload = str(kevin_num + 36)
|
||||
elif "Emote" in item_name:
|
||||
action = "UNLOCK_EMOTE"
|
||||
payload = str(item_table[item_name].code - item_table["Cooking Emote"].code)
|
||||
elif item_name == "Larger Tip Jar":
|
||||
action = "INC_TIP_COMBO"
|
||||
elif item_name == "Order Lookahead":
|
||||
action = "INC_ORDERS_ON_SCREEN"
|
||||
elif item_name == "Bonus Star":
|
||||
action = "INC_STAR_COUNT"
|
||||
payload = "1"
|
||||
elif item_name == "Progressive Dash":
|
||||
action = "INC_DASH"
|
||||
elif item_name == "Progressive Throw/Catch":
|
||||
action = "INC_THROW"
|
||||
else:
|
||||
config_name = item_name_to_config_name[item_name]
|
||||
vanilla_value = vanilla_values[config_name]
|
||||
|
||||
action = "SET_VALUE"
|
||||
payload = f"{config_name}={vanilla_value}"
|
||||
|
||||
return {
|
||||
"message": message,
|
||||
"action": action,
|
||||
"payload": payload,
|
||||
}
|
||||
@@ -0,0 +1,15 @@
|
||||
from BaseClasses import Location
|
||||
from .Overcooked2Levels import Overcooked2Level
|
||||
|
||||
|
||||
class Overcooked2Location(Location):
|
||||
game: str = "Overcooked! 2"
|
||||
|
||||
|
||||
oc2_location_name_to_id = dict()
|
||||
oc2_location_id_to_name = dict()
|
||||
for level in Overcooked2Level():
|
||||
if level.level_id == 36:
|
||||
continue # level 6-6 does not have an item location
|
||||
oc2_location_name_to_id[level.location_name_item] = level.level_id
|
||||
oc2_location_id_to_name[level.level_id] = level.location_name_item
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,110 @@
|
||||
from typing import TypedDict
|
||||
from Options import DefaultOnToggle, Range, Choice
|
||||
|
||||
|
||||
class OC2OnToggle(DefaultOnToggle):
|
||||
@property
|
||||
def result(self) -> bool:
|
||||
return bool(self.value)
|
||||
|
||||
|
||||
class AlwaysServeOldestOrder(OC2OnToggle):
|
||||
"""Modifies the game so that serving an expired order doesn't target the ticket with the highest tip. This helps players dig out of a broken tip combo faster."""
|
||||
display_name = "Always Serve Oldest Order"
|
||||
|
||||
|
||||
class AlwaysPreserveCookingProgress(OC2OnToggle):
|
||||
"""Modifies the game to behave more like AYCE, where adding an item to an in-progress container doesn't reset the entire progress bar."""
|
||||
display_name = "Preserve Cooking/Mixing Progress"
|
||||
|
||||
|
||||
class DisplayLeaderboardScores(OC2OnToggle):
|
||||
"""Modifies the Overworld map to fetch and display the current world records for each level. Press number keys 1-4 to view leaderboard scores for that number of players."""
|
||||
display_name = "Display Leaderboard Scores"
|
||||
|
||||
|
||||
class ShuffleLevelOrder(OC2OnToggle):
|
||||
"""Shuffles the order of kitchens on the overworld map. Also draws from DLC maps."""
|
||||
display_name = "Shuffle Level Order"
|
||||
|
||||
|
||||
class IncludeHordeLevels(OC2OnToggle):
|
||||
"""Includes "Horde Defence" levels in the pool of possible kitchens when Shuffle Level Order is enabled. Also adds two horde-specific items into the item pool."""
|
||||
display_name = "Include Horde Levels"
|
||||
|
||||
|
||||
class KevinLevels(OC2OnToggle):
|
||||
"""Includes the 8 Kevin level locations on the map as unlockables. Turn off to make games shorter."""
|
||||
display_name = "Kevin Level Checks"
|
||||
|
||||
|
||||
class FixBugs(OC2OnToggle):
|
||||
"""Fixes Bugs Present in the base game:
|
||||
- Double Serving Exploit
|
||||
- Sink Bug
|
||||
- Control Stick Cancel/Throw Bug
|
||||
- Can't Throw Near Empty Burner Bug"""
|
||||
display_name = "Fix Bugs"
|
||||
|
||||
|
||||
class ShorterLevelDuration(OC2OnToggle):
|
||||
"""Modifies level duration to be about 1/3rd shorter than in the original game, thus bringing the item discovery pace in line with other popular Archipelago games.
|
||||
|
||||
Points required to earn stars are scaled accordingly. ("Boss Levels" which change scenery mid-game are not affected.)"""
|
||||
display_name = "Shorter Level Duration"
|
||||
|
||||
|
||||
class PrepLevels(Choice):
|
||||
"""Choose How "Prep Levels" are handled (levels where the timer does not start until the first order is served):
|
||||
|
||||
- Original: Prep Levels may appear
|
||||
|
||||
- Excluded: Prep Levels are excluded from the pool during level shuffling
|
||||
|
||||
- All You Can Eat: Prep Levels may appear, but the timer automatically starts. The star score requirements are also adjusted to use the All You Can Eat World Record (if it exists)"""
|
||||
auto_display_name = True
|
||||
display_name = "Prep Level Behavior"
|
||||
option_original = 0
|
||||
option_excluded = 1
|
||||
option_all_you_can_eat = 2
|
||||
default = 1
|
||||
|
||||
|
||||
class StarsToWin(Range):
|
||||
"""Number of stars required to unlock 6-6.
|
||||
|
||||
Level purchase requirements between 1-1 and 6-6 will be spread between these two numbers. Using too high of a number may result in more frequent generation failures, especially when horde levels are enabled."""
|
||||
display_name = "Stars to Win"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 66
|
||||
|
||||
|
||||
class StarThresholdScale(Range):
|
||||
"""How difficult should the third star for each level be on a scale of 1-100%, where 100% is the current world record score and 45% is the average vanilla 4-star score."""
|
||||
display_name = "Star Difficulty %"
|
||||
range_start = 1
|
||||
range_end = 100
|
||||
default = 45
|
||||
|
||||
|
||||
overcooked_options = {
|
||||
# randomization options
|
||||
"shuffle_level_order": ShuffleLevelOrder,
|
||||
"include_horde_levels": IncludeHordeLevels,
|
||||
"prep_levels": PrepLevels,
|
||||
"kevin_levels": KevinLevels,
|
||||
|
||||
# quality of life options
|
||||
"fix_bugs": FixBugs,
|
||||
"shorter_level_duration": ShorterLevelDuration,
|
||||
"always_preserve_cooking_progress": AlwaysPreserveCookingProgress,
|
||||
"always_serve_oldest_order": AlwaysServeOldestOrder,
|
||||
"display_leaderboard_scores": DisplayLeaderboardScores,
|
||||
|
||||
# difficulty settings
|
||||
"stars_to_win": StarsToWin,
|
||||
"star_threshold_scale": StarThresholdScale,
|
||||
}
|
||||
|
||||
OC2Options = TypedDict("OC2Options", {option.__name__: option for option in overcooked_options.values()})
|
||||
@@ -0,0 +1,349 @@
|
||||
from enum import Enum
|
||||
from typing import List
|
||||
|
||||
|
||||
class Overcooked2Dlc(Enum):
|
||||
STORY = "Story"
|
||||
SURF_N_TURF = "Surf 'n' Turf"
|
||||
CAMPFIRE_COOK_OFF = "Campfire Cook Off"
|
||||
NIGHT_OF_THE_HANGRY_HORDE = "Night of the Hangry Horde"
|
||||
CARNIVAL_OF_CHAOS = "Carnival of Chaos"
|
||||
SEASONAL = "Seasonal"
|
||||
# CHRISTMAS = "Christmas"
|
||||
# CHINESE_NEW_YEAR = "Chinese New Year"
|
||||
# WINTER_WONDERLAND = "Winter Wonderland"
|
||||
# MOON_HARVEST = "Moon Harvest"
|
||||
# SPRING_FRESTIVAL = "Spring Festival"
|
||||
# SUNS_OUT_BUNS_OUT = "Sun's Out Buns Out"
|
||||
|
||||
def __int__(self) -> int:
|
||||
if self == Overcooked2Dlc.STORY:
|
||||
return 0
|
||||
if self == Overcooked2Dlc.SURF_N_TURF:
|
||||
return 1
|
||||
if self == Overcooked2Dlc.CAMPFIRE_COOK_OFF:
|
||||
return 2
|
||||
if self == Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE:
|
||||
return 3
|
||||
if self == Overcooked2Dlc.CARNIVAL_OF_CHAOS:
|
||||
return 4
|
||||
if self == Overcooked2Dlc.SEASONAL:
|
||||
return 5
|
||||
assert False
|
||||
|
||||
# inclusive
|
||||
def start_level_id(self) -> int:
|
||||
if self == Overcooked2Dlc.STORY:
|
||||
return 1
|
||||
return 0
|
||||
|
||||
# exclusive
|
||||
def end_level_id(self) -> int:
|
||||
id = None
|
||||
if self == Overcooked2Dlc.STORY:
|
||||
id = 6*6 + 8 # world_count*level_count + kevin count
|
||||
if self == Overcooked2Dlc.SURF_N_TURF:
|
||||
id = 3*4 + 1
|
||||
if self == Overcooked2Dlc.CAMPFIRE_COOK_OFF:
|
||||
id = 3*4 + 3
|
||||
if self == Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE:
|
||||
id = 3*3 + 3 + 8
|
||||
if self == Overcooked2Dlc.CARNIVAL_OF_CHAOS:
|
||||
id = 3*4 + 3
|
||||
if self == Overcooked2Dlc.SEASONAL:
|
||||
id = 31
|
||||
|
||||
return self.start_level_id() + id
|
||||
|
||||
# Tutorial + Horde Levels + Endgame
|
||||
def excluded_levels(self) -> List[int]:
|
||||
if self == Overcooked2Dlc.STORY:
|
||||
return [0, 36]
|
||||
|
||||
return []
|
||||
|
||||
def horde_levels(self) -> List[int]:
|
||||
if self == Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE:
|
||||
return [12, 13, 14, 15, 16, 17, 18, 19]
|
||||
if self == Overcooked2Dlc.SEASONAL:
|
||||
return [13, 15]
|
||||
|
||||
return []
|
||||
|
||||
def prep_levels(self) -> List[int]:
|
||||
if self == Overcooked2Dlc.STORY:
|
||||
return [1, 2, 5, 10, 12, 13, 28, 31]
|
||||
if self == Overcooked2Dlc.SURF_N_TURF:
|
||||
return [0, 4]
|
||||
if self == Overcooked2Dlc.CAMPFIRE_COOK_OFF:
|
||||
return [0, 2, 4, 9]
|
||||
if self == Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE:
|
||||
return [0, 1, 4]
|
||||
if self == Overcooked2Dlc.CARNIVAL_OF_CHAOS:
|
||||
return [0, 1, 3, 4, 5]
|
||||
if self == Overcooked2Dlc.SEASONAL:
|
||||
# moon 1-1 is a prep level for 1P only, but we can't make that assumption here
|
||||
return [0, 1, 5, 6, 12, 14, 16, 17, 18, 22, 23, 24, 27, 29]
|
||||
|
||||
return []
|
||||
|
||||
|
||||
class Overcooked2GameWorld(Enum):
|
||||
ONE = 1
|
||||
TWO = 2
|
||||
THREE = 3
|
||||
FOUR = 4
|
||||
FIVE = 5
|
||||
SIX = 6
|
||||
KEVIN = 7
|
||||
|
||||
@property
|
||||
def as_str(self) -> str:
|
||||
if self == Overcooked2GameWorld.KEVIN:
|
||||
return "Kevin"
|
||||
|
||||
return str(int(self.value))
|
||||
|
||||
@property
|
||||
def sublevel_count(self) -> int:
|
||||
if self == Overcooked2GameWorld.KEVIN:
|
||||
return 8
|
||||
|
||||
return 6
|
||||
|
||||
@property
|
||||
def base_id(self) -> int:
|
||||
if self == Overcooked2GameWorld.ONE:
|
||||
return 1
|
||||
|
||||
prev = Overcooked2GameWorld(self.value - 1)
|
||||
return prev.base_id + prev.sublevel_count
|
||||
|
||||
@property
|
||||
def name(self) -> str:
|
||||
if self == Overcooked2GameWorld.KEVIN:
|
||||
return "Kevin"
|
||||
|
||||
return "World " + self.as_str
|
||||
|
||||
|
||||
class Overcooked2GenericLevel():
|
||||
dlc: Overcooked2Dlc
|
||||
level_id: int
|
||||
|
||||
def __init__(self, level_id: int, dlc: Overcooked2Dlc = Overcooked2Dlc("Story")):
|
||||
self.dlc = dlc
|
||||
self.level_id = level_id
|
||||
|
||||
def __str__(self) -> str:
|
||||
return f"{self.dlc.value}|{self.level_id}"
|
||||
|
||||
def __repr__(self) -> str:
|
||||
return f"{self}"
|
||||
|
||||
@property
|
||||
def shortname(self) -> str:
|
||||
return level_id_to_shortname[(self.dlc, self.level_id)]
|
||||
|
||||
@property
|
||||
def is_horde(self) -> bool:
|
||||
return self.level_id in self.dlc.horde_levels()
|
||||
|
||||
|
||||
class Overcooked2Level:
|
||||
"""
|
||||
Abstraction for a playable levels in Overcooked 2. By default constructor
|
||||
it can be used as an iterator for all locations in the Story map.
|
||||
"""
|
||||
world: Overcooked2GameWorld
|
||||
sublevel: int
|
||||
|
||||
def __init__(self):
|
||||
self.world = Overcooked2GameWorld.ONE
|
||||
self.sublevel = 0
|
||||
|
||||
def __iter__(self):
|
||||
return self
|
||||
|
||||
def __next__(self):
|
||||
self.sublevel += 1
|
||||
if self.sublevel > self.world.sublevel_count:
|
||||
if self.world == Overcooked2GameWorld.KEVIN:
|
||||
raise StopIteration
|
||||
self.world = Overcooked2GameWorld(self.world.value + 1)
|
||||
self.sublevel = 1
|
||||
|
||||
return self
|
||||
|
||||
@property
|
||||
def level_id(self) -> int:
|
||||
return self.world.base_id + (self.sublevel - 1)
|
||||
|
||||
@property
|
||||
def level_name(self) -> str:
|
||||
return self.world.as_str + "-" + str(self.sublevel)
|
||||
|
||||
@property
|
||||
def location_name_item(self) -> str:
|
||||
return self.level_name + " Completed"
|
||||
|
||||
@property
|
||||
def location_name_level_complete(self) -> str:
|
||||
return self.level_name + " Level Completed"
|
||||
|
||||
@property
|
||||
def event_name_level_complete(self) -> str:
|
||||
return self.level_name + " Level Complete"
|
||||
|
||||
def location_name_star_event(self, stars: int) -> str:
|
||||
return "%s (%d-Star)" % (self.level_name, stars)
|
||||
|
||||
@property
|
||||
def as_generic_level(self) -> Overcooked2GenericLevel:
|
||||
return Overcooked2GenericLevel(self.level_id)
|
||||
|
||||
|
||||
# Note that there are valid levels beyond what is listed here, but they are all
|
||||
# Onion King Dialogs
|
||||
level_id_to_shortname = {
|
||||
(Overcooked2Dlc.STORY , 0 ): "Tutorial" ,
|
||||
(Overcooked2Dlc.STORY , 1 ): "Story 1-1" ,
|
||||
(Overcooked2Dlc.STORY , 2 ): "Story 1-2" ,
|
||||
(Overcooked2Dlc.STORY , 3 ): "Story 1-3" ,
|
||||
(Overcooked2Dlc.STORY , 4 ): "Story 1-4" ,
|
||||
(Overcooked2Dlc.STORY , 5 ): "Story 1-5" ,
|
||||
(Overcooked2Dlc.STORY , 6 ): "Story 1-6" ,
|
||||
(Overcooked2Dlc.STORY , 7 ): "Story 2-1" ,
|
||||
(Overcooked2Dlc.STORY , 8 ): "Story 2-2" ,
|
||||
(Overcooked2Dlc.STORY , 9 ): "Story 2-3" ,
|
||||
(Overcooked2Dlc.STORY , 10 ): "Story 2-4" ,
|
||||
(Overcooked2Dlc.STORY , 11 ): "Story 2-5" ,
|
||||
(Overcooked2Dlc.STORY , 12 ): "Story 2-6" ,
|
||||
(Overcooked2Dlc.STORY , 13 ): "Story 3-1" ,
|
||||
(Overcooked2Dlc.STORY , 14 ): "Story 3-2" ,
|
||||
(Overcooked2Dlc.STORY , 15 ): "Story 3-3" ,
|
||||
(Overcooked2Dlc.STORY , 16 ): "Story 3-4" ,
|
||||
(Overcooked2Dlc.STORY , 17 ): "Story 3-5" ,
|
||||
(Overcooked2Dlc.STORY , 18 ): "Story 3-6" ,
|
||||
(Overcooked2Dlc.STORY , 19 ): "Story 4-1" ,
|
||||
(Overcooked2Dlc.STORY , 20 ): "Story 4-2" ,
|
||||
(Overcooked2Dlc.STORY , 21 ): "Story 4-3" ,
|
||||
(Overcooked2Dlc.STORY , 22 ): "Story 4-4" ,
|
||||
(Overcooked2Dlc.STORY , 23 ): "Story 4-5" ,
|
||||
(Overcooked2Dlc.STORY , 24 ): "Story 4-6" ,
|
||||
(Overcooked2Dlc.STORY , 25 ): "Story 5-1" ,
|
||||
(Overcooked2Dlc.STORY , 26 ): "Story 5-2" ,
|
||||
(Overcooked2Dlc.STORY , 27 ): "Story 5-3" ,
|
||||
(Overcooked2Dlc.STORY , 28 ): "Story 5-4" ,
|
||||
(Overcooked2Dlc.STORY , 29 ): "Story 5-5" ,
|
||||
(Overcooked2Dlc.STORY , 30 ): "Story 5-6" ,
|
||||
(Overcooked2Dlc.STORY , 31 ): "Story 6-1" ,
|
||||
(Overcooked2Dlc.STORY , 32 ): "Story 6-2" ,
|
||||
(Overcooked2Dlc.STORY , 33 ): "Story 6-3" ,
|
||||
(Overcooked2Dlc.STORY , 34 ): "Story 6-4" ,
|
||||
(Overcooked2Dlc.STORY , 35 ): "Story 6-5" ,
|
||||
(Overcooked2Dlc.STORY , 36 ): "Story 6-6" ,
|
||||
(Overcooked2Dlc.STORY , 37 ): "Story K-1" ,
|
||||
(Overcooked2Dlc.STORY , 38 ): "Story K-2" ,
|
||||
(Overcooked2Dlc.STORY , 39 ): "Story K-3" ,
|
||||
(Overcooked2Dlc.STORY , 40 ): "Story K-4" ,
|
||||
(Overcooked2Dlc.STORY , 41 ): "Story K-5" ,
|
||||
(Overcooked2Dlc.STORY , 42 ): "Story K-6" ,
|
||||
(Overcooked2Dlc.STORY , 43 ): "Story K-7" ,
|
||||
(Overcooked2Dlc.STORY , 44 ): "Story K-8" ,
|
||||
(Overcooked2Dlc.SURF_N_TURF , 0 ): "Surf 1-1" ,
|
||||
(Overcooked2Dlc.SURF_N_TURF , 1 ): "Surf 1-2" ,
|
||||
(Overcooked2Dlc.SURF_N_TURF , 2 ): "Surf 1-3" ,
|
||||
(Overcooked2Dlc.SURF_N_TURF , 3 ): "Surf 1-4" ,
|
||||
(Overcooked2Dlc.SURF_N_TURF , 4 ): "Surf 2-1" ,
|
||||
(Overcooked2Dlc.SURF_N_TURF , 5 ): "Surf 2-2" ,
|
||||
(Overcooked2Dlc.SURF_N_TURF , 6 ): "Surf 2-3" ,
|
||||
(Overcooked2Dlc.SURF_N_TURF , 7 ): "Surf 2-4" ,
|
||||
(Overcooked2Dlc.SURF_N_TURF , 8 ): "Surf 3-1" ,
|
||||
(Overcooked2Dlc.SURF_N_TURF , 9 ): "Surf 3-2" ,
|
||||
(Overcooked2Dlc.SURF_N_TURF , 10 ): "Surf 3-3" ,
|
||||
(Overcooked2Dlc.SURF_N_TURF , 11 ): "Surf 3-4" ,
|
||||
(Overcooked2Dlc.SURF_N_TURF , 12 ): "Surf K-1" ,
|
||||
(Overcooked2Dlc.CAMPFIRE_COOK_OFF , 0 ): "Campfire 1-1" ,
|
||||
(Overcooked2Dlc.CAMPFIRE_COOK_OFF , 1 ): "Campfire 1-2" ,
|
||||
(Overcooked2Dlc.CAMPFIRE_COOK_OFF , 2 ): "Campfire 1-3" ,
|
||||
(Overcooked2Dlc.CAMPFIRE_COOK_OFF , 3 ): "Campfire 1-4" ,
|
||||
(Overcooked2Dlc.CAMPFIRE_COOK_OFF , 4 ): "Campfire 2-1" ,
|
||||
(Overcooked2Dlc.CAMPFIRE_COOK_OFF , 5 ): "Campfire 2-2" ,
|
||||
(Overcooked2Dlc.CAMPFIRE_COOK_OFF , 6 ): "Campfire 2-3" ,
|
||||
(Overcooked2Dlc.CAMPFIRE_COOK_OFF , 7 ): "Campfire 2-4" ,
|
||||
(Overcooked2Dlc.CAMPFIRE_COOK_OFF , 8 ): "Campfire 3-1" ,
|
||||
(Overcooked2Dlc.CAMPFIRE_COOK_OFF , 9 ): "Campfire 3-2" ,
|
||||
(Overcooked2Dlc.CAMPFIRE_COOK_OFF , 10 ): "Campfire 3-3" ,
|
||||
(Overcooked2Dlc.CAMPFIRE_COOK_OFF , 11 ): "Campfire 3-4" ,
|
||||
(Overcooked2Dlc.CAMPFIRE_COOK_OFF , 12 ): "Campfire K-1" ,
|
||||
(Overcooked2Dlc.CAMPFIRE_COOK_OFF , 13 ): "Campfire K-2" ,
|
||||
(Overcooked2Dlc.CAMPFIRE_COOK_OFF , 14 ): "Campfire K-3" ,
|
||||
(Overcooked2Dlc.CARNIVAL_OF_CHAOS , 0 ): "Carnival 1-1" ,
|
||||
(Overcooked2Dlc.CARNIVAL_OF_CHAOS , 1 ): "Carnival 1-2" ,
|
||||
(Overcooked2Dlc.CARNIVAL_OF_CHAOS , 2 ): "Carnival 1-3" ,
|
||||
(Overcooked2Dlc.CARNIVAL_OF_CHAOS , 3 ): "Carnival 1-4" ,
|
||||
(Overcooked2Dlc.CARNIVAL_OF_CHAOS , 4 ): "Carnival 2-1" ,
|
||||
(Overcooked2Dlc.CARNIVAL_OF_CHAOS , 5 ): "Carnival 2-2" ,
|
||||
(Overcooked2Dlc.CARNIVAL_OF_CHAOS , 6 ): "Carnival 2-3" ,
|
||||
(Overcooked2Dlc.CARNIVAL_OF_CHAOS , 7 ): "Carnival 2-4" ,
|
||||
(Overcooked2Dlc.CARNIVAL_OF_CHAOS , 8 ): "Carnival 3-1" ,
|
||||
(Overcooked2Dlc.CARNIVAL_OF_CHAOS , 9 ): "Carnival 3-2" ,
|
||||
(Overcooked2Dlc.CARNIVAL_OF_CHAOS , 10 ): "Carnival 3-3" ,
|
||||
(Overcooked2Dlc.CARNIVAL_OF_CHAOS , 11 ): "Carnival 3-4" ,
|
||||
(Overcooked2Dlc.CARNIVAL_OF_CHAOS , 12 ): "Carnival K-1" ,
|
||||
(Overcooked2Dlc.CARNIVAL_OF_CHAOS , 13 ): "Carnival K-2" ,
|
||||
(Overcooked2Dlc.CARNIVAL_OF_CHAOS , 14 ): "Carnival K-3" ,
|
||||
(Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 0 ): "Horde 1-1" ,
|
||||
(Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 1 ): "Horde 1-2" ,
|
||||
(Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 2 ): "Horde 1-3" ,
|
||||
(Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 3 ): "Horde 2-1" ,
|
||||
(Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 4 ): "Horde 2-2" ,
|
||||
(Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 5 ): "Horde 2-3" ,
|
||||
(Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 6 ): "Horde 3-1" ,
|
||||
(Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 7 ): "Horde 3-2" ,
|
||||
(Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 8 ): "Horde 3-3" ,
|
||||
(Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 9 ): "Horde K-1" ,
|
||||
(Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 10 ): "Horde K-2" ,
|
||||
(Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 11 ): "Horde K-3" ,
|
||||
(Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 12 ): "Horde H-1" ,
|
||||
(Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 13 ): "Horde H-2" ,
|
||||
(Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 14 ): "Horde H-3" ,
|
||||
(Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 15 ): "Horde H-4" ,
|
||||
(Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 16 ): "Horde H-5" ,
|
||||
(Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 17 ): "Horde H-6" ,
|
||||
(Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 18 ): "Horde H-7" ,
|
||||
(Overcooked2Dlc.NIGHT_OF_THE_HANGRY_HORDE , 19 ): "Horde H-8" ,
|
||||
(Overcooked2Dlc.SEASONAL , 0 ): "Christmas 1-1" ,
|
||||
(Overcooked2Dlc.SEASONAL , 1 ): "Christmas 1-2" ,
|
||||
(Overcooked2Dlc.SEASONAL , 2 ): "Christmas 1-3" ,
|
||||
(Overcooked2Dlc.SEASONAL , 3 ): "Christmas 1-4" ,
|
||||
(Overcooked2Dlc.SEASONAL , 4 ): "Christmas 1-5" ,
|
||||
(Overcooked2Dlc.SEASONAL , 5 ): "Chinese 1-1" ,
|
||||
(Overcooked2Dlc.SEASONAL , 6 ): "Chinese 1-2" ,
|
||||
(Overcooked2Dlc.SEASONAL , 7 ): "Chinese 1-3" ,
|
||||
(Overcooked2Dlc.SEASONAL , 8 ): "Chinese 1-4" ,
|
||||
(Overcooked2Dlc.SEASONAL , 9 ): "Chinese 1-5" ,
|
||||
(Overcooked2Dlc.SEASONAL , 10 ): "Chinese 1-6" ,
|
||||
(Overcooked2Dlc.SEASONAL , 11 ): "Chinese 1-7" ,
|
||||
(Overcooked2Dlc.SEASONAL , 12 ): "Winter 1-1" ,
|
||||
(Overcooked2Dlc.SEASONAL , 13 ): "Winter H-2" ,
|
||||
(Overcooked2Dlc.SEASONAL , 14 ): "Winter 1-3" ,
|
||||
(Overcooked2Dlc.SEASONAL , 15 ): "Winter H-4" ,
|
||||
(Overcooked2Dlc.SEASONAL , 16 ): "Winter 1-5" ,
|
||||
(Overcooked2Dlc.SEASONAL , 17 ): "Spring 1-1" ,
|
||||
(Overcooked2Dlc.SEASONAL , 18 ): "Spring 1-2" ,
|
||||
(Overcooked2Dlc.SEASONAL , 19 ): "Spring 1-3" ,
|
||||
(Overcooked2Dlc.SEASONAL , 20 ): "Spring 1-4" ,
|
||||
(Overcooked2Dlc.SEASONAL , 21 ): "Spring 1-5" ,
|
||||
(Overcooked2Dlc.SEASONAL , 22 ): "SOBO 1-1" ,
|
||||
(Overcooked2Dlc.SEASONAL , 23 ): "SOBO 1-2" ,
|
||||
(Overcooked2Dlc.SEASONAL , 24 ): "SOBO 1-3" ,
|
||||
(Overcooked2Dlc.SEASONAL , 25 ): "SOBO 1-4" ,
|
||||
(Overcooked2Dlc.SEASONAL , 26 ): "SOBO 1-5" ,
|
||||
(Overcooked2Dlc.SEASONAL , 27 ): "Moon 1-1" ,
|
||||
(Overcooked2Dlc.SEASONAL , 28 ): "Moon 1-2" ,
|
||||
(Overcooked2Dlc.SEASONAL , 29 ): "Moon 1-3" ,
|
||||
(Overcooked2Dlc.SEASONAL , 30 ): "Moon 1-4" ,
|
||||
(Overcooked2Dlc.SEASONAL , 31 ): "Moon 1-5" ,
|
||||
}
|
||||
@@ -0,0 +1,510 @@
|
||||
from enum import Enum
|
||||
from typing import Callable, Dict, Any, List, Optional
|
||||
|
||||
from BaseClasses import ItemClassification, CollectionState, Region, Entrance, Location, RegionType, Tutorial
|
||||
from worlds.AutoWorld import World, WebWorld
|
||||
|
||||
from .Overcooked2Levels import Overcooked2Level, Overcooked2GenericLevel
|
||||
from .Locations import Overcooked2Location, oc2_location_name_to_id, oc2_location_id_to_name
|
||||
from .Options import overcooked_options, OC2Options, OC2OnToggle
|
||||
from .Items import item_table, Overcooked2Item, item_name_to_id, item_id_to_name, item_to_unlock_event, item_frequencies
|
||||
from .Logic import has_requirements_for_level_star, has_requirements_for_level_access, level_shuffle_factory, is_item_progression, is_useful
|
||||
|
||||
|
||||
class Overcooked2Web(WebWorld):
|
||||
theme = "partyTime"
|
||||
|
||||
bug_report_page = "https://github.com/toasterparty/oc2-modding/issues"
|
||||
setup_en = Tutorial(
|
||||
"Multiworld Setup Tutorial",
|
||||
"A guide to setting up the Overcooked! 2 randomizer on your computer.",
|
||||
"English",
|
||||
"setup_en.md",
|
||||
"setup/en",
|
||||
["toasterparty"]
|
||||
)
|
||||
|
||||
tutorials = [setup_en]
|
||||
|
||||
|
||||
class PrepLevelMode(Enum):
|
||||
original = 0
|
||||
excluded = 1
|
||||
ayce = 2
|
||||
|
||||
|
||||
class Overcooked2World(World):
|
||||
"""
|
||||
Overcooked! 2 is a franticly paced arcade cooking game where
|
||||
players race against the clock to complete orders for points. Bring
|
||||
peace to the Onion Kingdom once again by recovering lost items and abilities,
|
||||
earning stars to unlock levels, and defeating the unbread horde. Levels are
|
||||
randomized to increase gameplay variety. Play with up to 4 friends.
|
||||
"""
|
||||
|
||||
# Autoworld API
|
||||
|
||||
game = "Overcooked! 2"
|
||||
web = Overcooked2Web()
|
||||
required_client_version = (0, 3, 4)
|
||||
option_definitions = overcooked_options
|
||||
topology_present: bool = False
|
||||
remote_items: bool = True
|
||||
remote_start_inventory: bool = False
|
||||
data_version = 2
|
||||
|
||||
item_name_to_id = item_name_to_id
|
||||
item_id_to_name = item_id_to_name
|
||||
|
||||
location_id_to_name = oc2_location_id_to_name
|
||||
location_name_to_id = oc2_location_name_to_id
|
||||
|
||||
options: Dict[str, Any]
|
||||
itempool: List[Overcooked2Item]
|
||||
|
||||
|
||||
# Helper Functions
|
||||
|
||||
def is_level_horde(self, level_id: int) -> bool:
|
||||
return self.options["IncludeHordeLevels"] and \
|
||||
(self.level_mapping is not None) and \
|
||||
level_id in self.level_mapping.keys() and \
|
||||
self.level_mapping[level_id].is_horde
|
||||
|
||||
def create_item(self, item: str, classification: ItemClassification = ItemClassification.progression) -> Overcooked2Item:
|
||||
return Overcooked2Item(item, classification, self.item_name_to_id[item], self.player)
|
||||
|
||||
def create_event(self, event: str, classification: ItemClassification) -> Overcooked2Item:
|
||||
return Overcooked2Item(event, classification, None, self.player)
|
||||
|
||||
def place_event(self, location_name: str, item_name: str,
|
||||
classification: ItemClassification = ItemClassification.progression_skip_balancing):
|
||||
location: Location = self.world.get_location(location_name, self.player)
|
||||
location.place_locked_item(self.create_event(item_name, classification))
|
||||
|
||||
def add_region(self, region_name: str):
|
||||
region = Region(
|
||||
region_name,
|
||||
RegionType.Generic,
|
||||
region_name,
|
||||
self.player,
|
||||
self.world,
|
||||
)
|
||||
self.world.regions.append(region)
|
||||
|
||||
def connect_regions(self, source: str, target: str, rule: Optional[Callable[[CollectionState], bool]] = None):
|
||||
sourceRegion = self.world.get_region(source, self.player)
|
||||
targetRegion = self.world.get_region(target, self.player)
|
||||
|
||||
connection = Entrance(self.player, '', sourceRegion)
|
||||
if rule:
|
||||
connection.access_rule = rule
|
||||
|
||||
sourceRegion.exits.append(connection)
|
||||
connection.connect(targetRegion)
|
||||
|
||||
def add_level_location(
|
||||
self,
|
||||
region_name: str,
|
||||
location_name: str,
|
||||
level_id: int,
|
||||
stars: int,
|
||||
is_event: bool = False,
|
||||
) -> None:
|
||||
|
||||
if is_event:
|
||||
location_id = None
|
||||
else:
|
||||
location_id = level_id
|
||||
|
||||
region = self.world.get_region(region_name, self.player)
|
||||
location = Overcooked2Location(
|
||||
self.player,
|
||||
location_name,
|
||||
location_id,
|
||||
region,
|
||||
)
|
||||
|
||||
location.event = is_event
|
||||
|
||||
# if level_id is none, then it's the 6-6 edge case
|
||||
if level_id is None:
|
||||
level_id = 36
|
||||
if self.level_mapping is not None and level_id in self.level_mapping:
|
||||
level = self.level_mapping[level_id]
|
||||
else:
|
||||
level = Overcooked2GenericLevel(level_id)
|
||||
|
||||
completion_condition: Callable[[CollectionState], bool] = \
|
||||
lambda state, level=level, stars=stars: \
|
||||
has_requirements_for_level_star(state, level, stars, self.player)
|
||||
location.access_rule = completion_condition
|
||||
|
||||
region.locations.append(
|
||||
location
|
||||
)
|
||||
|
||||
def get_options(self) -> Dict[str, Any]:
|
||||
return OC2Options({option.__name__: getattr(self.world, name)[self.player].result
|
||||
if issubclass(option, OC2OnToggle) else getattr(self.world, name)[self.player].value
|
||||
for name, option in overcooked_options.items()})
|
||||
|
||||
# Helper Data
|
||||
|
||||
level_unlock_counts: Dict[int, int] # level_id, stars to purchase
|
||||
level_mapping: Dict[int, Overcooked2GenericLevel] # level_id, level
|
||||
|
||||
# Autoworld Hooks
|
||||
|
||||
def generate_early(self):
|
||||
self.options = self.get_options()
|
||||
|
||||
# 0.0 to 1.0 where 1.0 is World Record
|
||||
self.star_threshold_scale = self.options["StarThresholdScale"] / 100.0
|
||||
|
||||
# Generate level unlock requirements such that the levels get harder to unlock
|
||||
# the further the game has progressed, and levels progress radially rather than linearly
|
||||
self.level_unlock_counts = level_unlock_requirement_factory(self.options["StarsToWin"])
|
||||
|
||||
# Assign new kitchens to each spot on the overworld using pure random chance and nothing else
|
||||
if self.options["ShuffleLevelOrder"]:
|
||||
self.level_mapping = \
|
||||
level_shuffle_factory(
|
||||
self.world.random,
|
||||
self.options["PrepLevels"] != PrepLevelMode.excluded.value,
|
||||
self.options["IncludeHordeLevels"],
|
||||
)
|
||||
else:
|
||||
self.level_mapping = None
|
||||
|
||||
def create_regions(self) -> None:
|
||||
# Menu -> Overworld
|
||||
self.add_region("Menu")
|
||||
self.add_region("Overworld")
|
||||
self.connect_regions("Menu", "Overworld")
|
||||
|
||||
for level in Overcooked2Level():
|
||||
if not self.options["KevinLevels"] and level.level_id > 36:
|
||||
break
|
||||
|
||||
# Create Region (e.g. "1-1")
|
||||
self.add_region(level.level_name)
|
||||
|
||||
# Add Location to house progression item (1-star)
|
||||
if level.level_id == 36:
|
||||
# 6-6 doesn't have progression, but it does have victory condition which is placed later
|
||||
self.add_level_location(
|
||||
level.level_name,
|
||||
level.location_name_item,
|
||||
None,
|
||||
1,
|
||||
)
|
||||
else:
|
||||
# Location to house progression item
|
||||
self.add_level_location(
|
||||
level.level_name,
|
||||
level.location_name_item,
|
||||
level.level_id,
|
||||
1,
|
||||
)
|
||||
|
||||
# Location to house level completed event
|
||||
self.add_level_location(
|
||||
level.level_name,
|
||||
level.location_name_level_complete,
|
||||
level.level_id,
|
||||
1,
|
||||
is_event=True,
|
||||
)
|
||||
|
||||
# Add Locations to house star aquisition events, except for horde levels
|
||||
if not self.is_level_horde(level.level_id):
|
||||
for n in [1, 2, 3]:
|
||||
self.add_level_location(
|
||||
level.level_name,
|
||||
level.location_name_star_event(n),
|
||||
level.level_id,
|
||||
n,
|
||||
is_event=True,
|
||||
)
|
||||
|
||||
# Overworld -> Level
|
||||
required_star_count: int = self.level_unlock_counts[level.level_id]
|
||||
if level.level_id % 6 != 1 and level.level_id <= 36:
|
||||
previous_level_completed_event_name: str = Overcooked2GenericLevel(
|
||||
level.level_id - 1).shortname.split(" ")[1] + " Level Complete"
|
||||
else:
|
||||
previous_level_completed_event_name = None
|
||||
|
||||
level_access_rule: Callable[[CollectionState], bool] = \
|
||||
lambda state, level_name=level.level_name, previous_level_completed_event_name=previous_level_completed_event_name, required_star_count=required_star_count: \
|
||||
has_requirements_for_level_access(state, level_name, previous_level_completed_event_name, required_star_count, self.player)
|
||||
self.connect_regions("Overworld", level.level_name, level_access_rule)
|
||||
|
||||
# Level --> Overworld
|
||||
self.connect_regions(level.level_name, "Overworld")
|
||||
|
||||
completion_condition: Callable[[CollectionState], bool] = lambda state: \
|
||||
state.has("Victory", self.player)
|
||||
self.world.completion_condition[self.player] = completion_condition
|
||||
|
||||
def create_items(self):
|
||||
self.itempool = []
|
||||
|
||||
# Make Items
|
||||
# useful = list()
|
||||
# filler = list()
|
||||
# progression = list()
|
||||
for item_name in item_table:
|
||||
if not self.options["IncludeHordeLevels"] and item_name in ["Calmer Unbread", "Coin Purse"]:
|
||||
# skip items which are irrelevant to the seed
|
||||
continue
|
||||
|
||||
if not self.options["KevinLevels"] and item_name.startswith("Kevin"):
|
||||
continue
|
||||
|
||||
if is_item_progression(item_name, self.level_mapping, self.options["KevinLevels"]):
|
||||
# print(f"{item_name} is progression")
|
||||
# progression.append(item_name)
|
||||
classification = ItemClassification.progression
|
||||
else:
|
||||
# print(f"{item_name} is filler")
|
||||
if (is_useful(item_name)):
|
||||
# useful.append(item_name)
|
||||
classification = ItemClassification.useful
|
||||
else:
|
||||
# filler.append(item_name)
|
||||
classification = ItemClassification.filler
|
||||
|
||||
if item_name in item_frequencies:
|
||||
freq = item_frequencies[item_name]
|
||||
|
||||
while freq > 0:
|
||||
self.itempool.append(self.create_item(item_name, classification))
|
||||
classification = ItemClassification.useful # only the first progressive item can be progression
|
||||
freq -= 1
|
||||
else:
|
||||
self.itempool.append(self.create_item(item_name, classification))
|
||||
|
||||
# print(f"progression: {progression}")
|
||||
# print(f"useful: {useful}")
|
||||
# print(f"filler: {filler}")
|
||||
|
||||
# Fill any free space with filler
|
||||
pool_count = len(oc2_location_name_to_id)
|
||||
if not self.options["KevinLevels"]:
|
||||
pool_count -= 8
|
||||
|
||||
while len(self.itempool) < pool_count:
|
||||
self.itempool.append(self.create_item("Bonus Star", ItemClassification.useful))
|
||||
|
||||
self.world.itempool += self.itempool
|
||||
|
||||
def set_rules(self):
|
||||
pass
|
||||
|
||||
def generate_basic(self) -> None:
|
||||
# Add Events (Star Acquisition)
|
||||
for level in Overcooked2Level():
|
||||
if not self.options["KevinLevels"] and level.level_id > 36:
|
||||
break
|
||||
|
||||
if level.level_id != 36:
|
||||
self.place_event(level.location_name_level_complete, level.event_name_level_complete)
|
||||
|
||||
if self.is_level_horde(level.level_id):
|
||||
continue # horde levels don't have star rewards
|
||||
|
||||
for n in [1, 2, 3]:
|
||||
self.place_event(level.location_name_star_event(n), "Star")
|
||||
|
||||
# Add Victory Condition
|
||||
self.place_event("6-6 Completed", "Victory")
|
||||
|
||||
# Items get distributed to locations
|
||||
|
||||
def fill_json_data(self) -> Dict[str, Any]:
|
||||
mod_name = f"AP-{self.world.seed_name}-P{self.player}-{self.world.player_name[self.player]}"
|
||||
|
||||
# Serialize Level Order
|
||||
story_level_order = dict()
|
||||
|
||||
if self.options["ShuffleLevelOrder"]:
|
||||
for level_id in self.level_mapping:
|
||||
level: Overcooked2GenericLevel = self.level_mapping[level_id]
|
||||
story_level_order[str(level_id)] = {
|
||||
"DLC": level.dlc.value,
|
||||
"LevelID": level.level_id,
|
||||
}
|
||||
|
||||
custom_level_order = dict()
|
||||
custom_level_order["Story"] = story_level_order
|
||||
|
||||
# Serialize Unlock Requirements
|
||||
level_purchase_requirements = dict()
|
||||
for level_id in self.level_unlock_counts:
|
||||
level_purchase_requirements[str(level_id)] = self.level_unlock_counts[level_id]
|
||||
|
||||
# Override Vanilla Unlock Chain Behavior
|
||||
# (all worlds accessible from the start and progressible in any order)
|
||||
level_unlock_requirements = dict()
|
||||
level_force_reveal = [
|
||||
1, # 1-1
|
||||
7, # 2-1
|
||||
13, # 3-1
|
||||
19, # 4-1
|
||||
25, # 5-1
|
||||
31, # 6-1
|
||||
]
|
||||
for level_id in range(1, 37):
|
||||
if (level_id not in level_force_reveal):
|
||||
level_unlock_requirements[str(level_id)] = level_id - 1
|
||||
|
||||
# Set Kevin Unlock Requirements
|
||||
if self.options["KevinLevels"]:
|
||||
def kevin_level_to_keyholder_level_id(level_id: int) -> Optional[int]:
|
||||
location = self.world.find_item(f"Kevin-{level_id-36}", self.player)
|
||||
if location.player != self.player:
|
||||
return None # This kevin level will be unlocked by the server at runtime
|
||||
level_id = oc2_location_name_to_id[location.name]
|
||||
return level_id
|
||||
|
||||
for level_id in range(37, 45):
|
||||
keyholder_level_id = kevin_level_to_keyholder_level_id(level_id)
|
||||
if keyholder_level_id is not None:
|
||||
level_unlock_requirements[str(level_id)] = keyholder_level_id
|
||||
|
||||
# Place Items at Level Completion Screens (local only)
|
||||
on_level_completed: Dict[str, list[Dict[str, str]]] = dict()
|
||||
regions = self.world.get_regions(self.player)
|
||||
for region in regions:
|
||||
for location in region.locations:
|
||||
if location.item is None:
|
||||
continue
|
||||
if location.item.code is None:
|
||||
continue # it's an event
|
||||
if location.item.player != self.player:
|
||||
continue # not for us
|
||||
level_id = str(oc2_location_name_to_id[location.name])
|
||||
on_level_completed[level_id] = [item_to_unlock_event(location.item.name)]
|
||||
|
||||
# Put it all together
|
||||
star_threshold_scale = self.options["StarThresholdScale"] / 100
|
||||
|
||||
base_data = {
|
||||
# Changes Inherent to rando
|
||||
"DisableAllMods": False,
|
||||
"UnlockAllChefs": True,
|
||||
"UnlockAllDLC": True,
|
||||
"DisplayFPS": True,
|
||||
"SkipTutorial": True,
|
||||
"SkipAllOnionKing": True,
|
||||
"SkipTutorialPopups": True,
|
||||
"RevealAllLevels": False,
|
||||
"PurchaseAllLevels": False,
|
||||
"CheatsEnabled": False,
|
||||
"ImpossibleTutorial": True,
|
||||
"ForbidDLC": True,
|
||||
"ForceSingleSaveSlot": True,
|
||||
"DisableNGP": True,
|
||||
"LevelForceReveal": level_force_reveal,
|
||||
"SaveFolderName": mod_name,
|
||||
"CustomOrderTimeoutPenalty": 10,
|
||||
"LevelForceHide": [37, 38, 39, 40, 41, 42, 43, 44],
|
||||
|
||||
# Game Modifications
|
||||
"LevelPurchaseRequirements": level_purchase_requirements,
|
||||
"Custom66TimerScale": max(0.4, (1.0 - star_threshold_scale)),
|
||||
|
||||
"CustomLevelOrder": custom_level_order,
|
||||
|
||||
# Items (Starting Inventory)
|
||||
"CustomOrderLifetime": 70.0, # 100 is original
|
||||
"DisableWood": True,
|
||||
"DisableCoal": True,
|
||||
"DisableOnePlate": True,
|
||||
"DisableFireExtinguisher": True,
|
||||
"DisableBellows": True,
|
||||
"PlatesStartDirty": True,
|
||||
"MaxTipCombo": 2,
|
||||
"DisableDash": True,
|
||||
"WeakDash": True,
|
||||
"DisableThrow": True,
|
||||
"DisableCatch": True,
|
||||
"DisableControlStick": True,
|
||||
"DisableWokDrag": True,
|
||||
"DisableRampButton": True,
|
||||
"WashTimeMultiplier": 1.4,
|
||||
"BurnSpeedMultiplier": 1.43,
|
||||
"MaxOrdersOnScreenOffset": -2,
|
||||
"ChoppingTimeScale": 1.4,
|
||||
"BackpackMovementScale": 0.75,
|
||||
"RespawnTime": 10.0,
|
||||
"CarnivalDispenserRefactoryTime": 4.0,
|
||||
"LevelUnlockRequirements": level_unlock_requirements,
|
||||
"LockedEmotes": [0, 1, 2, 3, 4, 5],
|
||||
"StarOffset": 0,
|
||||
"AggressiveHorde": True,
|
||||
"DisableEarnHordeMoney": True,
|
||||
|
||||
# Item Unlocking
|
||||
"OnLevelCompleted": on_level_completed,
|
||||
}
|
||||
|
||||
# Set remaining data in the options dict
|
||||
bugs = ["FixDoubleServing", "FixSinkBug", "FixControlStickThrowBug", "FixEmptyBurnerThrow"]
|
||||
for bug in bugs:
|
||||
self.options[bug] = self.options["FixBugs"]
|
||||
self.options["PreserveCookingProgress"] = self.options["AlwaysPreserveCookingProgress"]
|
||||
self.options["TimerAlwaysStarts"] = self.options["PrepLevels"] == PrepLevelMode.ayce.value
|
||||
self.options["LevelTimerScale"] = 0.666 if self.options["ShorterLevelDuration"] else 1.0
|
||||
self.options["LeaderboardScoreScale"] = {
|
||||
"FourStars": 1.0,
|
||||
"ThreeStars": star_threshold_scale,
|
||||
"TwoStars": star_threshold_scale * 0.75,
|
||||
"OneStar": star_threshold_scale * 0.35,
|
||||
}
|
||||
|
||||
base_data.update(self.options)
|
||||
return base_data
|
||||
|
||||
def fill_slot_data(self) -> Dict[str, Any]:
|
||||
return self.fill_json_data()
|
||||
|
||||
|
||||
def level_unlock_requirement_factory(stars_to_win: int) -> Dict[int, int]:
|
||||
level_unlock_counts = dict()
|
||||
level = 1
|
||||
sublevel = 1
|
||||
for n in range(1, 37):
|
||||
progress: float = float(n)/36.0
|
||||
progress *= progress # x^2 curve
|
||||
|
||||
star_count = int(progress*float(stars_to_win))
|
||||
min = (n-1)*3
|
||||
if (star_count > min):
|
||||
star_count = min
|
||||
|
||||
level_id = (level-1)*6 + sublevel
|
||||
|
||||
# print("%d-%d (%d) = %d" % (level, sublevel, level_id, star_count))
|
||||
|
||||
level_unlock_counts[level_id] = star_count
|
||||
|
||||
level += 1
|
||||
if level > 6:
|
||||
level = 1
|
||||
sublevel += 1
|
||||
|
||||
# force sphere 1 to 0 stars to help keep our promises to the item fill algo
|
||||
level_unlock_counts[1] = 0 # 1-1
|
||||
level_unlock_counts[7] = 0 # 2-1
|
||||
level_unlock_counts[19] = 0 # 4-1
|
||||
|
||||
# Force 5-1 into sphere 1 to help things out
|
||||
level_unlock_counts[25] = 1 # 5-1
|
||||
|
||||
for n in range(37, 46):
|
||||
level_unlock_counts[n] = 0
|
||||
|
||||
return level_unlock_counts
|
||||
@@ -0,0 +1,86 @@
|
||||
# Overcooked! 2
|
||||
|
||||
## Quick Links
|
||||
- [Setup Guide](../../../../tutorial/Overcooked!%202/setup/en)
|
||||
- [Settings Page](../../../../games/Overcooked!%202/player-settings)
|
||||
- [OC2-Modding GitHub](https://github.com/toasterparty/oc2-modding)
|
||||
|
||||
## How Does Randomizer Work in the Kitchen?
|
||||
|
||||
The *Overcooked! 2* Randomizer completely transforms the game into a metroidvania with items and item locations. Many of the Chefs' inherent abilities have been temporarily removed such that your scoring potential is limited at the start of the game. The more your inventory grows, the easier it will be to earn 2 and 3 Stars on each level.
|
||||
|
||||
The game takes place entirely in the "Story" campaign on a fresh save file. The ultimate goal is to reach and complete level 6-6. In order to do this you must regain enough of your abilities to complete all levels in World 6 and obtain enough stars to purchase 6-6*.
|
||||
|
||||
Randomizer can be played alone (one player switches between controlling two chefs) or up to 4 local/online friends. Player count can be changed at any time during the Archipelago game.
|
||||
|
||||
**Note: 6-6 is excluded from "Shuffle Level Order", so it will always be the standard final boss stage.*
|
||||
|
||||
## Items
|
||||
|
||||
The first time a level is completed, a random item is given to the chef(s). If playing in a MultiWorld, completing a level may instead give another Archipelago user their item. The item found is displayed as text at the top of the results screen.
|
||||
|
||||
Once all items have been obtained, the game will play like the original experience.
|
||||
|
||||
The following items were invented for Randomizer:
|
||||
|
||||
### Player Abilities
|
||||
- Dash/Dash Cooldown
|
||||
- Throw/Catch
|
||||
- Sharp Knife
|
||||
- Dish Scrubber
|
||||
- Control Stick Batteries
|
||||
- Lightweight Backpack
|
||||
- Faster Respawn Time
|
||||
- Emote (x6)
|
||||
|
||||
### Objects
|
||||
- Spare Plate
|
||||
- Clean Dishes
|
||||
- Wood
|
||||
- Coal Bucket
|
||||
- Bellows
|
||||
- Fire Extinguisher
|
||||
|
||||
### Kitchen/Environment
|
||||
- Larger Tip Jar
|
||||
- Guest Patience
|
||||
- Burn Leniency
|
||||
- Faster Condiment & Drink Switch
|
||||
- Wok Wheels
|
||||
- Coin Purse
|
||||
- Calmer Unbread
|
||||
|
||||
### Overworld
|
||||
- Unlock Kevin Level (x8)
|
||||
- Ramp Button
|
||||
- Bonus Star (Filler Item*)
|
||||
|
||||
**Note: Bonus star count varies with settings*
|
||||
|
||||
## Other Game Modifications
|
||||
|
||||
In addition to shuffling items, the following changes are applied to the game:
|
||||
|
||||
### Quality of Life
|
||||
- Tutorial is skipped
|
||||
- Non-linear level order
|
||||
- "Auto-Complete" feature to finish a level early when a target score is obtained
|
||||
- Bugfixes for issues present in the base game (including "Sink Bug" and "Double Serving")
|
||||
- All chef avatars automatically unlocked
|
||||
- Optionally, level time can be reduced to make progression faster paced
|
||||
|
||||
### Randomization Options
|
||||
|
||||
- *Shuffle Level Order*
|
||||
- Replaces each level on the overworld with a random level
|
||||
- DLC levels can show up on the Story Overworld
|
||||
- Optionally exclude "Horde" Levels
|
||||
- Optionally exclude "Prep" Levels
|
||||
|
||||
### Difficulty Adjustments
|
||||
- Stars required to unlock levels have been rebalanced
|
||||
- Points required to earn stars have been rebalanced
|
||||
- Based off of the current World Record on the game's [Leaderboard](https://overcooked.greeny.dev)
|
||||
- 1-Star/2-Star scores are much closer to the 3-Star Score
|
||||
- Significantly reduced the time allotted to beat the final level
|
||||
- Reduced penalty for expired order
|
||||
@@ -0,0 +1,84 @@
|
||||
# Overcooked! 2 Randomizer Setup Guide
|
||||
|
||||
## Quick Links
|
||||
- [Main Page](../../../../games/Overcooked!%202/info/en)
|
||||
- [Settings Page](../../../../games/Overcooked!%202/player-settings)
|
||||
- [OC2-Modding GitHub](https://github.com/toasterparty/oc2-modding)
|
||||
|
||||
## Required Software
|
||||
|
||||
- Windows 10+
|
||||
- [Overcooked! 2](https://store.steampowered.com/bundle/13608/Overcooked_2___Gourmet_Edition/) for PC
|
||||
- **Steam: Recommended**
|
||||
- Steam (Beta Branch): Supported
|
||||
- Epic Games: Supported
|
||||
- GOG: Not officially supported - Adventurous users may choose to experiment at their own risk
|
||||
- Windows Store (aka GamePass): Not Supported
|
||||
- Xbox/PS/Switch: Not Supported
|
||||
- [OC2-Modding Client](https://github.com/toasterparty/oc2-modding/releases) (instructions below)
|
||||
|
||||
## Overview
|
||||
|
||||
*OC2-Modding* is a general purpose modding framework which doubles as an Archipelago MultiWorld Client. It works by using Harmony to inject custom code into the game at runtime, so none of the orignal game files need to be modified in any way.
|
||||
|
||||
When connecting to an Archipelago session using the in-game login screen, a modfile containing all relevant game modifications is automatically downloaded and applied.
|
||||
|
||||
From this point, the game will communicate with the Archipelago service directly to manage sending/receiving items. Notifications of important events will appear through an in-game console at the top of the screen.
|
||||
|
||||
## Overcooked! 2 Modding Guide
|
||||
|
||||
### Install
|
||||
|
||||
1. Download and extract the contents of the latest [OC2-Modding Release](https://github.com/toasterparty/oc2-modding/releases) anywhere on your PC
|
||||
|
||||
2. Double-Click **oc2-modding-install.bat** follow the instructions.
|
||||
|
||||
Once *OC2-Modding* is installed, you have successfully installed everything you need to play/participate in Archipelago MultiWorld games.
|
||||
|
||||
### Disable
|
||||
|
||||
To temporarily turn off *OC2-Modding* and return to the original game, open **...\Overcooked! 2\BepInEx\config\OC2Modding.cfg** in a text editor like notepad and edit the following:
|
||||
|
||||
`DisableAllMods = true`
|
||||
|
||||
To re-enable, simply change the word **true** back to a **false**.
|
||||
|
||||
### Uninstall
|
||||
|
||||
To completely remove *OC2-Modding*, navigate to your game's installation folder and run **oc2-modding-uninstall.bat**.
|
||||
|
||||
## Generate a MultiWorld Game
|
||||
|
||||
1. Visit the [Player Settings](../../../../games/Overcooked!%202/player-settings) page and configure the game-specific settings to taste
|
||||
|
||||
2. Export your yaml file and use it to generate a new randomized game
|
||||
- (For instructions on how to generate an Archipelago game, refer to the [Archipelago Web Guide](../../../../tutorial/Archipelago/using_website/en))
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
1. Launch the game
|
||||
|
||||
2. When attempting to enter the main menu from the title screen, the game will freeze and prompt you to sign in:
|
||||
|
||||

|
||||
|
||||
3. Sign-in with server address, username and password of the corresponding room you would like to join.
|
||||
- Otherwise, if you just want to play the vanilla game without any modifications, you may press "Continue without Archipelago" button.
|
||||
|
||||
4. Upon successful connection to the Archipelago service, you will be granted access to the main menu. The game will act as though you are playing for the first time. ***DO NOT FEAR*** — your original save data has not been overwritten; the Overcooked Randomizer just uses a temporary directory for it's save game data.
|
||||
|
||||
## Playing Co-Op
|
||||
|
||||
- To play local multiplayer (or Parsec/"Steam Play Together"), simply add the additional player to your game session as you would in the base game
|
||||
|
||||
- To play online multiplayer, the guest *must* also have the same version of OC2-Modding installed. In order for the game to work, the guest must sign in using the same information the host used to connect to the Archipelago session. Once both host and client are both connected, they may join one another in-game and proceed as normal. It does not matter who hosts the game, and the game's hosts may be changed at any point. You may notice some things are different when playing this way:
|
||||
|
||||
- Guests will still receive Archipelago messages about sent/received items the same as the host
|
||||
|
||||
- When the host loads the campaign, any connected guests are forced to select "Don't Save" when prompted to pick which save slot to use. This is because randomizer uses the Archipelago service as a pseudo "cloud save", so progress will always be synchronized between all participants of that randomized *Overcooked! 2* instance.
|
||||
|
||||
## Auto-Complete
|
||||
|
||||
Since the goal of randomizer isn't necessarily to achieve new personal high scores, players may find themselves waiting for a level timer to expire once they've met their objective. A new feature called *Auto-Complete* has been added to automatically complete levels once a target star count has been achieved.
|
||||
|
||||
To enable *Auto-Complete*, press the **Show** button near the top of your screen to expand the modding controls. Then, repeatedly press the **Auto-Complete** button until it shows the desired setting.
|
||||
@@ -0,0 +1,21 @@
|
||||
MIT License
|
||||
|
||||
Copyright (c) 2022 Alex "Alchav" Avery
|
||||
|
||||
Permission is hereby granted, free of charge, to any person obtaining a copy
|
||||
of this software and associated documentation files (the "Software"), to deal
|
||||
in the Software without restriction, including without limitation the rights
|
||||
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
||||
copies of the Software, and to permit persons to whom the Software is
|
||||
furnished to do so, subject to the following conditions:
|
||||
|
||||
The above copyright notice and this permission notice shall be included in all
|
||||
copies or substantial portions of the Software.
|
||||
|
||||
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
||||
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
||||
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
||||
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
||||
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
||||
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
|
||||
SOFTWARE.
|
||||
@@ -0,0 +1,252 @@
|
||||
from typing import TextIO
|
||||
import os
|
||||
import logging
|
||||
|
||||
from BaseClasses import Item, MultiWorld, Tutorial, ItemClassification
|
||||
from Fill import fill_restrictive, FillError, sweep_from_pool
|
||||
from ..AutoWorld import World, WebWorld
|
||||
from ..generic.Rules import add_item_rule
|
||||
from .items import item_table, item_groups
|
||||
from .locations import location_data, PokemonRBLocation
|
||||
from .regions import create_regions
|
||||
from .logic import PokemonLogic
|
||||
from .options import pokemon_rb_options
|
||||
from .rom_addresses import rom_addresses
|
||||
from .text import encode_text
|
||||
from .rom import generate_output, get_base_rom_bytes, get_base_rom_path, process_pokemon_data, process_wild_pokemon,\
|
||||
process_static_pokemon
|
||||
from .rules import set_rules
|
||||
|
||||
import worlds.pokemon_rb.poke_data as poke_data
|
||||
|
||||
|
||||
class PokemonWebWorld(WebWorld):
|
||||
tutorials = [Tutorial(
|
||||
"Multiworld Setup Guide",
|
||||
"A guide to playing Pokemon Red and Blue with Archipelago.",
|
||||
"English",
|
||||
"setup_en.md",
|
||||
"setup/en",
|
||||
["Alchav"]
|
||||
)]
|
||||
|
||||
|
||||
class PokemonRedBlueWorld(World):
|
||||
"""Pokémon Red and Pokémon Blue are the original monster-collecting turn-based RPGs. Explore the Kanto region with
|
||||
your Pokémon, catch more than 150 unique creatures, earn badges from the region's Gym Leaders, and challenge the
|
||||
Elite Four to become the champion!"""
|
||||
# -MuffinJets#4559
|
||||
game = "Pokemon Red and Blue"
|
||||
option_definitions = pokemon_rb_options
|
||||
remote_items = False
|
||||
data_version = 1
|
||||
topology_present = False
|
||||
|
||||
item_name_to_id = {name: data.id for name, data in item_table.items()}
|
||||
location_name_to_id = {location.name: location.address for location in location_data if location.type == "Item"}
|
||||
item_name_groups = item_groups
|
||||
|
||||
web = PokemonWebWorld()
|
||||
|
||||
def __init__(self, world: MultiWorld, player: int):
|
||||
super().__init__(world, player)
|
||||
self.fly_map = None
|
||||
self.fly_map_code = None
|
||||
self.extra_badges = {}
|
||||
self.type_chart = None
|
||||
self.local_poke_data = None
|
||||
self.learnsets = None
|
||||
self.trainer_name = None
|
||||
self.rival_name = None
|
||||
|
||||
@classmethod
|
||||
def stage_assert_generate(cls, world):
|
||||
versions = set()
|
||||
for player in world.player_ids:
|
||||
if world.worlds[player].game == "Pokemon Red and Blue":
|
||||
versions.add(world.game_version[player].current_key)
|
||||
for version in versions:
|
||||
if not os.path.exists(get_base_rom_path(version)):
|
||||
raise FileNotFoundError(get_base_rom_path(version))
|
||||
|
||||
def generate_early(self):
|
||||
def encode_name(name, t):
|
||||
try:
|
||||
if len(encode_text(name)) > 7:
|
||||
raise IndexError(f"{t} name too long for player {self.world.player_name[self.player]}. Must be 7 characters or fewer.")
|
||||
return encode_text(name, length=8, whitespace="@", safety=True)
|
||||
except KeyError as e:
|
||||
raise KeyError(f"Invalid character(s) in {t} name for player {self.world.player_name[self.player]}") from e
|
||||
self.trainer_name = encode_name(self.world.trainer_name[self.player].value, "Player")
|
||||
self.rival_name = encode_name(self.world.rival_name[self.player].value, "Rival")
|
||||
|
||||
if self.world.badges_needed_for_hm_moves[self.player].value >= 2:
|
||||
badges_to_add = ["Marsh Badge", "Volcano Badge", "Earth Badge"]
|
||||
if self.world.badges_needed_for_hm_moves[self.player].value == 3:
|
||||
badges = ["Boulder Badge", "Cascade Badge", "Thunder Badge", "Rainbow Badge", "Marsh Badge",
|
||||
"Soul Badge", "Volcano Badge", "Earth Badge"]
|
||||
self.world.random.shuffle(badges)
|
||||
badges_to_add += [badges.pop(), badges.pop()]
|
||||
hm_moves = ["Cut", "Fly", "Surf", "Strength", "Flash"]
|
||||
self.world.random.shuffle(hm_moves)
|
||||
self.extra_badges = {}
|
||||
for badge in badges_to_add:
|
||||
self.extra_badges[hm_moves.pop()] = badge
|
||||
|
||||
process_pokemon_data(self)
|
||||
|
||||
def create_items(self) -> None:
|
||||
locations = [location for location in location_data if location.type == "Item"]
|
||||
item_pool = []
|
||||
for location in locations:
|
||||
if "Hidden" in location.name and not self.world.randomize_hidden_items[self.player].value:
|
||||
continue
|
||||
if "Rock Tunnel B1F" in location.region and not self.world.extra_key_items[self.player].value:
|
||||
continue
|
||||
if location.name == "Celadon City - Mansion Lady" and not self.world.tea[self.player].value:
|
||||
continue
|
||||
item = self.create_item(location.original_item)
|
||||
if location.event:
|
||||
self.world.get_location(location.name, self.player).place_locked_item(item)
|
||||
elif ("Badge" not in item.name or self.world.badgesanity[self.player].value) and \
|
||||
(item.name != "Oak's Parcel" or self.world.old_man[self.player].value != 1):
|
||||
item_pool.append(item)
|
||||
self.world.random.shuffle(item_pool)
|
||||
|
||||
self.world.itempool += item_pool
|
||||
|
||||
|
||||
def pre_fill(self):
|
||||
|
||||
process_wild_pokemon(self)
|
||||
process_static_pokemon(self)
|
||||
|
||||
if self.world.old_man[self.player].value == 1:
|
||||
item = self.create_item("Oak's Parcel")
|
||||
locations = []
|
||||
for location in self.world.get_locations():
|
||||
if location.player == self.player and location.item is None and location.can_reach(self.world.state) \
|
||||
and location.item_rule(item):
|
||||
locations.append(location)
|
||||
self.world.random.choice(locations).place_locked_item(item)
|
||||
|
||||
if not self.world.badgesanity[self.player].value:
|
||||
self.world.non_local_items[self.player].value -= self.item_name_groups["Badges"]
|
||||
for i in range(5):
|
||||
try:
|
||||
badges = []
|
||||
badgelocs = []
|
||||
for badge in ["Boulder Badge", "Cascade Badge", "Thunder Badge", "Rainbow Badge", "Soul Badge",
|
||||
"Marsh Badge", "Volcano Badge", "Earth Badge"]:
|
||||
badges.append(self.create_item(badge))
|
||||
for loc in ["Pewter Gym - Brock 1", "Cerulean Gym - Misty 1", "Vermilion Gym - Lt. Surge 1",
|
||||
"Celadon Gym - Erika 1", "Fuchsia Gym - Koga 1", "Saffron Gym - Sabrina 1",
|
||||
"Cinnabar Gym - Blaine 1", "Viridian Gym - Giovanni 1"]:
|
||||
badgelocs.append(self.world.get_location(loc, self.player))
|
||||
state = self.world.get_all_state(False)
|
||||
self.world.random.shuffle(badges)
|
||||
self.world.random.shuffle(badgelocs)
|
||||
fill_restrictive(self.world, state, badgelocs.copy(), badges, True, True)
|
||||
except FillError:
|
||||
for location in badgelocs:
|
||||
location.item = None
|
||||
continue
|
||||
break
|
||||
else:
|
||||
raise FillError(f"Failed to place badges for player {self.player}")
|
||||
|
||||
locs = [self.world.get_location("Fossil - Choice A", self.player),
|
||||
self.world.get_location("Fossil - Choice B", self.player)]
|
||||
for loc in locs:
|
||||
add_item_rule(loc, lambda i: i.advancement or i.name in self.item_name_groups["Unique"]
|
||||
or i.name == "Master Ball")
|
||||
|
||||
loc = self.world.get_location("Pallet Town - Player's PC", self.player)
|
||||
if loc.item is None:
|
||||
locs.append(loc)
|
||||
|
||||
for loc in locs:
|
||||
unplaced_items = []
|
||||
if loc.name in self.world.priority_locations[self.player].value:
|
||||
add_item_rule(loc, lambda i: i.advancement)
|
||||
for item in self.world.itempool:
|
||||
if item.player == self.player and loc.item_rule(item):
|
||||
self.world.itempool.remove(item)
|
||||
state = sweep_from_pool(self.world.state, self.world.itempool + unplaced_items)
|
||||
if state.can_reach(loc, "Location", self.player):
|
||||
loc.place_locked_item(item)
|
||||
break
|
||||
else:
|
||||
unplaced_items.append(item)
|
||||
self.world.itempool += unplaced_items
|
||||
|
||||
intervene = False
|
||||
test_state = self.world.get_all_state(False)
|
||||
if not test_state.pokemon_rb_can_surf(self.player) or not test_state.pokemon_rb_can_strength(self.player):
|
||||
intervene = True
|
||||
elif self.world.accessibility[self.player].current_key != "minimal":
|
||||
if not test_state.pokemon_rb_can_cut(self.player) or not test_state.pokemon_rb_can_flash(self.player):
|
||||
intervene = True
|
||||
if intervene:
|
||||
# the way this is handled will be improved significantly in the future when I add options to
|
||||
# let you choose the exact weights for HM compatibility
|
||||
logging.warning(
|
||||
f"HM-compatible Pokémon possibly missing, placing Mew on Route 1 for player {self.player}")
|
||||
loc = self.world.get_location("Route 1 - Wild Pokemon - 1", self.player)
|
||||
loc.item = self.create_item("Mew")
|
||||
|
||||
def create_regions(self):
|
||||
if self.world.free_fly_location[self.player].value:
|
||||
fly_map_code = self.world.random.randint(5, 9)
|
||||
if fly_map_code == 9:
|
||||
fly_map_code = 10
|
||||
if fly_map_code == 5:
|
||||
fly_map_code = 4
|
||||
else:
|
||||
fly_map_code = 0
|
||||
self.fly_map = ["Pallet Town", "Viridian City", "Pewter City", "Cerulean City", "Lavender Town",
|
||||
"Vermilion City", "Celadon City", "Fuchsia City", "Cinnabar Island", "Indigo Plateau",
|
||||
"Saffron City"][fly_map_code]
|
||||
self.fly_map_code = fly_map_code
|
||||
create_regions(self.world, self.player)
|
||||
self.world.completion_condition[self.player] = lambda state, player=self.player: state.has("Become Champion", player=player)
|
||||
|
||||
def set_rules(self):
|
||||
set_rules(self.world, self.player)
|
||||
|
||||
def create_item(self, name: str) -> Item:
|
||||
return PokemonRBItem(name, self.player)
|
||||
|
||||
def generate_output(self, output_directory: str):
|
||||
generate_output(self, output_directory)
|
||||
|
||||
def write_spoiler_header(self, spoiler_handle: TextIO):
|
||||
if self.world.free_fly_location[self.player].value:
|
||||
spoiler_handle.write('Fly unlocks: %s\n' % self.fly_map)
|
||||
if self.extra_badges:
|
||||
for hm_move, badge in self.extra_badges.items():
|
||||
spoiler_handle.write(hm_move + " enabled by: " + (" " * 20)[:20 - len(hm_move)] + badge + "\n")
|
||||
|
||||
def write_spoiler(self, spoiler_handle):
|
||||
if self.world.randomize_type_matchup_types[self.player].value or \
|
||||
self.world.randomize_type_matchup_type_effectiveness[self.player].value:
|
||||
spoiler_handle.write(f"\n\nType matchups ({self.world.player_name[self.player]}):\n\n")
|
||||
for matchup in self.type_chart:
|
||||
spoiler_handle.write(f"{matchup[0]} deals {matchup[2] * 10}% damage to {matchup[1]}\n")
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
return self.world.random.choice([item for item in item_table if item_table[item].classification in
|
||||
[ItemClassification.filler, ItemClassification.trap]])
|
||||
|
||||
|
||||
class PokemonRBItem(Item):
|
||||
game = "Pokemon Red and Blue"
|
||||
type = None
|
||||
|
||||
def __init__(self, name, player: int = None):
|
||||
item_data = item_table[name]
|
||||
super(PokemonRBItem, self).__init__(
|
||||
name,
|
||||
item_data.classification,
|
||||
item_data.id, player
|
||||
)
|
||||
Binary file not shown.
Binary file not shown.
@@ -0,0 +1,55 @@
|
||||
# Pokémon Red and Blue
|
||||
|
||||
## Where is the settings page?
|
||||
|
||||
The [player settings page for this game](../player-settings) contains all the options you need to configure and export a
|
||||
config file.
|
||||
|
||||
## What does randomization do to this game?
|
||||
|
||||
Items which the player would normally acquire throughout the game have been moved around. Logic remains, so the game is
|
||||
always able to be completed, but because of the item shuffle the player may need to access certain areas before they
|
||||
would in the vanilla game.
|
||||
|
||||
A great many things besides item placement can be randomized, such as the location of Pokémon, their stats, types, etc., depending on your yaml settings.
|
||||
|
||||
Many baseline changes are made to the game, including:
|
||||
|
||||
* Bag item space increased to 128 slots (up from 20)
|
||||
* PC item storage increased to 64 slots (up from 50)
|
||||
* You can hold B to run (or bike extra fast!).
|
||||
* You can hold select while talking to a trainer to re-battle them.
|
||||
* You can return to route 2 from Diglett's Cave without the use of Cut.
|
||||
* Mew can be encountered at the S.S. Anne dock truck. This can be randomized depending on your settings.
|
||||
* The S.S. Anne will never depart.
|
||||
* Seafoam Islands entrances are swapped. This means you need Strength to travel through from Cinnabar Island to Fuchsia
|
||||
City
|
||||
* After obtaining one of the fossil item checks in Mt Moon, the remaining item can be received from the Cinnabar Lab
|
||||
fossil scientist. This may require reviving a number of fossils, depending on your settings.
|
||||
* Obedience depends on the total number of badges you have obtained instead of depending on specific badges.
|
||||
* Pokémon that evolve by trading can also evolve by reaching level 35.
|
||||
* Evolution stones are reusable.
|
||||
* Much of the dialogue throughout the game has been removed or shortened.
|
||||
* If the Old Man is blocking your way through Viridian City, you do not have Oak's Parcel in your inventory, and you've
|
||||
exhausted your money and Poké Balls, you can get a free Poké Ball from your mom.
|
||||
|
||||
## What items and locations get shuffled?
|
||||
|
||||
All items that go into your bags given by NPCs or found on the ground, as well as gym badges.
|
||||
Optionally, hidden items (those located with the Item Finder) can be shuffled as well.
|
||||
|
||||
## 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.
|
||||
By default, gym badges are shuffled across only the 8 gyms, but you can turn on Badgesanity in your yaml to shuffle them
|
||||
into the general item pool.
|
||||
|
||||
## What does another world's item look like in Pokémon Red and Blue?
|
||||
|
||||
All items for other games will display simply as "AP ITEM," including those for other Pokémon Red and Blue games.
|
||||
|
||||
## When the player receives an item, what happens?
|
||||
|
||||
A "received item" sound effect will play. Currently, there is no in-game message informing you of what the item is.
|
||||
If you are in battle, have menus or text boxes opened, or scripted events are occurring, the items will not be given to
|
||||
you until these have ended.
|
||||
@@ -0,0 +1,84 @@
|
||||
# Setup Guide for Pokémon Red and Blue: Archipelago
|
||||
|
||||
## Important
|
||||
|
||||
As we are using Bizhawk, this guide is only applicable to Windows and Linux systems.
|
||||
|
||||
## Required Software
|
||||
|
||||
- Bizhawk: [Bizhawk Releases from TASVideos](https://tasvideos.org/BizHawk/ReleaseHistory)
|
||||
- Version 2.3.1 and later are supported. Version 2.7 is recommended for stability.
|
||||
- Detailed installation instructions for Bizhawk can be found at the above link.
|
||||
- Windows users must run the prereq installer first, which can also be found at the above link.
|
||||
- The built-in Archipelago client, which can be installed [here](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
(select `Pokemon Client` during installation).
|
||||
- Pokémon Red and/or Blue ROM files. The Archipelago community cannot provide these.
|
||||
|
||||
## Configuring Bizhawk
|
||||
|
||||
Once Bizhawk has been installed, open Bizhawk and change the following settings:
|
||||
|
||||
- Under Config > Customize > Advanced, make sure the box for AutoSaveRAM is checked, and click the 5s button.
|
||||
This reduces the possibility of losing save data in emulator crashes.
|
||||
- Under Config > Customize, check the "Run in background" and "Accept background input" boxes. This will allow you to
|
||||
continue playing in the background, even if another window is selected.
|
||||
|
||||
It is strongly recommended to associate GB rom extensions (\*.gb) to the Bizhawk we've just installed.
|
||||
To do so, we simply have to search any Gameboy rom we happened to own, right click and select "Open with...", unfold
|
||||
the list that appears and select the bottom option "Look for another application", then browse to the Bizhawk folder
|
||||
and select EmuHawk.exe.
|
||||
|
||||
## 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 generate a yaml or download a template by visiting the [Pokemon Red and Blue Player Settings Page](/games/Pokemon Red and Blue/player-settings)
|
||||
|
||||
It is important to note that the `game_version` option determines the ROM file that will be patched.
|
||||
Both the player and the person generating (if they are generating locally) will need the corresponding ROM file.
|
||||
|
||||
For `trainer_name` and `rival_name` the following regular characters are allowed:
|
||||
|
||||
* `‘’“”·… ABCDEFGHIJKLMNOPQRSTUVWXYZ():;[]abcdefghijklmnopqrstuvwxyzé'-?!.♂$×/,♀0123456789`
|
||||
|
||||
And the following special characters (these each take up one character):
|
||||
* `<'d>`
|
||||
* `<'l>`
|
||||
* `<'t>`
|
||||
* `<'v>`
|
||||
* `<'r>`
|
||||
* `<'m>`
|
||||
* `<PK>`
|
||||
* `<MN>`
|
||||
* `<MALE>` alias for `♂`
|
||||
* `<FEMALE>` alias for `♀`
|
||||
|
||||
## Joining a MultiWorld Game
|
||||
|
||||
### Obtain your Pokémon patch file
|
||||
|
||||
When you join a multiworld game, you will be asked to provide your YAML file to whoever is hosting. Once that is done,
|
||||
the host will provide you with either a link to download your data file, or with a zip file containing everyone's data
|
||||
files. Your data file should have a `.apred` or `.apblue` extension.
|
||||
|
||||
Double-click on your patch file to start your client and start the ROM patch process. Once the process is finished
|
||||
(this can take a while), the client and the emulator will be started automatically (if you associated the extension
|
||||
to the emulator as recommended).
|
||||
|
||||
### Connect to the Multiserver
|
||||
|
||||
Once both the client and the emulator are started, you must connect them. Within the emulator click on the "Tools"
|
||||
menu and select "Lua Console". Click the folder button or press Ctrl+O to open a Lua script.
|
||||
|
||||
Navigate to your Archipelago install folder and open `data/lua/PKMN_RB/pkmr_rb.lua`.
|
||||
|
||||
To connect the client to the multiserver simply put `<address>:<port>` on the textfield on top and press enter (if the
|
||||
server uses password, type in the bottom textfield `/connect <address>:<port> [password]`)
|
||||
|
||||
Now you are ready to start your adventure in Kanto.
|
||||
@@ -0,0 +1,176 @@
|
||||
from BaseClasses import ItemClassification
|
||||
from .poke_data import pokemon_data
|
||||
|
||||
class ItemData:
|
||||
def __init__(self, id, classification, groups):
|
||||
self.groups = groups
|
||||
self.classification = classification
|
||||
self.id = None if id is None else id + 172000000
|
||||
|
||||
item_table = {
|
||||
"Master Ball": ItemData(1, ItemClassification.useful, ["Consumables", "Poke Balls"]),
|
||||
"Ultra Ball": ItemData(2, ItemClassification.filler, ["Consumables", "Poke Balls"]),
|
||||
"Great Ball": ItemData(3, ItemClassification.filler, ["Consumables", "Poke Balls"]),
|
||||
"Poke Ball": ItemData(4, ItemClassification.filler, ["Consumables", "Poke Balls"]),
|
||||
"Town Map": ItemData(5, ItemClassification.progression_skip_balancing, ["Unique", "Key Items"]),
|
||||
"Bicycle": ItemData(6, ItemClassification.progression, ["Unique", "Key Items"]),
|
||||
# "Flippers": ItemData(7, ItemClassification.progression),
|
||||
#"Safari Ball": ItemData(8, ItemClassification.filler),
|
||||
#"Pokedex": ItemData(9, ItemClassification.filler),
|
||||
"Moon Stone": ItemData(10, ItemClassification.useful, ["Unique", "Evolution Stones"]),
|
||||
"Antidote": ItemData(11, ItemClassification.filler, ["Consumables"]),
|
||||
"Burn Heal": ItemData(12, ItemClassification.filler, ["Consumables"]),
|
||||
"Ice Heal": ItemData(13, ItemClassification.filler, ["Consumables"]),
|
||||
"Awakening": ItemData(14, ItemClassification.filler, ["Consumables"]),
|
||||
"Paralyze Heal": ItemData(15, ItemClassification.filler, ["Consumables"]),
|
||||
"Full Restore": ItemData(16, ItemClassification.filler, ["Consumables"]),
|
||||
"Max Potion": ItemData(17, ItemClassification.filler, ["Consumables"]),
|
||||
"Hyper Potion": ItemData(18, ItemClassification.filler, ["Consumables"]),
|
||||
"Super Potion": ItemData(19, ItemClassification.filler, ["Consumables"]),
|
||||
"Potion": ItemData(20, ItemClassification.filler, ["Consumables"]),
|
||||
"Boulder Badge": ItemData(21, ItemClassification.progression, ["Unique", "Key Items", "Badges"]),
|
||||
"Cascade Badge": ItemData(22, ItemClassification.progression, ["Unique", "Key Items", "Badges"]),
|
||||
"Thunder Badge": ItemData(23, ItemClassification.progression, ["Unique", "Key Items", "Badges"]),
|
||||
"Rainbow Badge": ItemData(24, ItemClassification.progression, ["Unique", "Key Items", "Badges"]),
|
||||
"Soul Badge": ItemData(25, ItemClassification.progression, ["Unique", "Key Items", "Badges"]),
|
||||
"Marsh Badge": ItemData(26, ItemClassification.progression, ["Unique", "Key Items", "Badges"]),
|
||||
"Volcano Badge": ItemData(27, ItemClassification.progression, ["Unique", "Key Items", "Badges"]),
|
||||
"Earth Badge": ItemData(28, ItemClassification.progression, ["Unique", "Key Items", "Badges"]),
|
||||
"Escape Rope": ItemData(29, ItemClassification.filler, ["Consumables"]),
|
||||
"Repel": ItemData(30, ItemClassification.filler, ["Consumables"]),
|
||||
"Old Amber": ItemData(31, ItemClassification.progression_skip_balancing, ["Unique", "Fossils"]),
|
||||
"Fire Stone": ItemData(32, ItemClassification.useful, ["Unique", "Evolution Stones"]),
|
||||
"Thunder Stone": ItemData(33, ItemClassification.useful, ["Unique", "Evolution Stones"]),
|
||||
"Water Stone": ItemData(34, ItemClassification.useful, ["Unique", "Evolution Stones"]),
|
||||
"HP Up": ItemData(35, ItemClassification.filler, ["Consumables", "Vitamins"]),
|
||||
"Protein": ItemData(36, ItemClassification.filler, ["Consumables", "Vitamins"]),
|
||||
"Iron": ItemData(37, ItemClassification.filler, ["Consumables", "Vitamins"]),
|
||||
"Carbos": ItemData(38, ItemClassification.filler, ["Consumables", "Vitamins"]),
|
||||
"Calcium": ItemData(39, ItemClassification.filler, ["Consumables", "Vitamins"]),
|
||||
"Rare Candy": ItemData(40, ItemClassification.useful, ["Consumables"]),
|
||||
"Dome Fossil": ItemData(41, ItemClassification.progression_skip_balancing, ["Unique", "Fossils"]),
|
||||
"Helix Fossil": ItemData(42, ItemClassification.progression_skip_balancing, ["Unique", "Fossils"]),
|
||||
"Secret Key": ItemData(43, ItemClassification.progression, ["Unique", "Key Items"]),
|
||||
"Bike Voucher": ItemData(45, ItemClassification.progression, ["Unique", "Key Items"]),
|
||||
"X Accuracy": ItemData(46, ItemClassification.filler, ["Consumables", "Battle Items"]),
|
||||
"Leaf Stone": ItemData(47, ItemClassification.useful, ["Unique", "Evolution Stones"]),
|
||||
"Card Key": ItemData(48, ItemClassification.progression, ["Unique", "Key Items"]),
|
||||
"Nugget": ItemData(49, ItemClassification.filler, []),
|
||||
#"Laptop": ItemData(50, ItemClassification.useful, ["Unique"]),
|
||||
"Poke Doll": ItemData(51, ItemClassification.filler, ["Consumables"]),
|
||||
"Full Heal": ItemData(52, ItemClassification.filler, ["Consumables"]),
|
||||
"Revive": ItemData(53, ItemClassification.filler, ["Consumables"]),
|
||||
"Max Revive": ItemData(54, ItemClassification.filler, ["Consumables"]),
|
||||
"Guard Spec": ItemData(55, ItemClassification.filler, ["Consumables", "Battle Items"]),
|
||||
"Super Repel": ItemData(56, ItemClassification.filler, ["Consumables"]),
|
||||
"Max Repel": ItemData(57, ItemClassification.filler, ["Consumables"]),
|
||||
"Dire Hit": ItemData(58, ItemClassification.filler, ["Consumables", "Battle Items"]),
|
||||
#"Coin": ItemData(59, ItemClassification.filler),
|
||||
"Fresh Water": ItemData(60, ItemClassification.filler, ["Consumables"]),
|
||||
"Soda Pop": ItemData(61, ItemClassification.filler, ["Consumables"]),
|
||||
"Lemonade": ItemData(62, ItemClassification.filler, ["Consumables"]),
|
||||
"S.S. Ticket": ItemData(63, ItemClassification.progression, ["Unique", "Key Items"]),
|
||||
"Gold Teeth": ItemData(64, ItemClassification.progression, ["Unique", "Key Items"]),
|
||||
"X Attack": ItemData(65, ItemClassification.filler, ["Consumables", "Battle Items"]),
|
||||
"X Defend": ItemData(66, ItemClassification.filler, ["Consumables", "Battle Items"]),
|
||||
"X Speed": ItemData(67, ItemClassification.filler, ["Consumables", "Battle Items"]),
|
||||
"X Special": ItemData(68, ItemClassification.filler, ["Consumables", "Battle Items"]),
|
||||
"Coin Case": ItemData(69, ItemClassification.progression_skip_balancing, ["Unique", "Key Items"]),
|
||||
"Oak's Parcel": ItemData(70, ItemClassification.progression, ["Unique", "Key Items"]),
|
||||
"Item Finder": ItemData(71, ItemClassification.progression, ["Unique", "Key Items"]),
|
||||
"Silph Scope": ItemData(72, ItemClassification.progression, ["Unique", "Key Items"]),
|
||||
"Poke Flute": ItemData(73, ItemClassification.progression, ["Unique", "Key Items"]),
|
||||
"Lift Key": ItemData(74, ItemClassification.progression, ["Unique", "Key Items"]),
|
||||
"Exp. All": ItemData(75, ItemClassification.useful, ["Unique"]),
|
||||
"Old Rod": ItemData(76, ItemClassification.progression_skip_balancing, ["Unique", "Key Items", "Rods"]),
|
||||
"Good Rod": ItemData(77, ItemClassification.progression_skip_balancing, ["Unique", "Key Items", "Rods"]),
|
||||
"Super Rod": ItemData(78, ItemClassification.progression_skip_balancing, ["Unique", "Key Items", "Rods"]),
|
||||
"PP Up": ItemData(79, ItemClassification.filler, ["Consumables"]),
|
||||
"Ether": ItemData(80, ItemClassification.filler, ["Consumables"]),
|
||||
"Max Ether": ItemData(81, ItemClassification.filler, ["Consumables"]),
|
||||
"Elixir": ItemData(82, ItemClassification.filler, ["Consumables"]),
|
||||
"Max Elixir": ItemData(83, ItemClassification.filler, ["Consumables"]),
|
||||
"Tea": ItemData(84, ItemClassification.progression, ["Unique", "Key Items"]),
|
||||
# "Master Sword": ItemData(85, ItemClassification.progression),
|
||||
# "Flute": ItemData(86, ItemClassification.progression),
|
||||
# "Titan's Mitt": ItemData(87, ItemClassification.progression),
|
||||
# "Lamp": ItemData(88, ItemClassification.progression),
|
||||
"Plant Key": ItemData(89, ItemClassification.progression, ["Unique", "Key Items"]),
|
||||
"Mansion Key": ItemData(90, ItemClassification.progression, ["Unique", "Key Items"]),
|
||||
"Hideout Key": ItemData(91, ItemClassification.progression, ["Unique", "Key Items"]),
|
||||
"Safari Pass": ItemData(93, ItemClassification.progression, ["Unique", "Key Items"]),
|
||||
"HM01 Cut": ItemData(196, ItemClassification.progression, ["Unique", "HMs"]),
|
||||
"HM02 Fly": ItemData(197, ItemClassification.progression, ["Unique", "HMs"]),
|
||||
"HM03 Surf": ItemData(198, ItemClassification.progression, ["Unique", "HMs"]),
|
||||
"HM04 Strength": ItemData(199, ItemClassification.progression, ["Unique", "HMs"]),
|
||||
"HM05 Flash": ItemData(200, ItemClassification.progression, ["Unique", "HMs"]),
|
||||
"TM01 Mega Punch": ItemData(201, ItemClassification.useful, ["Unique", "TMs"]),
|
||||
"TM02 Razor Wind": ItemData(202, ItemClassification.useful, ["Unique", "TMs"]),
|
||||
"TM03 Swords Dance": ItemData(203, ItemClassification.useful, ["Unique", "TMs"]),
|
||||
"TM04 Whirlwind": ItemData(204, ItemClassification.filler, ["Unique", "TMs"]),
|
||||
"TM05 Mega Kick": ItemData(205, ItemClassification.useful, ["Unique", "TMs"]),
|
||||
"TM06 Toxic": ItemData(206, ItemClassification.useful, ["Unique", "TMs"]),
|
||||
"TM07 Horn Drill": ItemData(207, ItemClassification.useful, ["Unique", "TMs"]),
|
||||
"TM08 Body Slam": ItemData(208, ItemClassification.useful, ["Unique", "TMs"]),
|
||||
"TM09 Take Down": ItemData(209, ItemClassification.useful, ["Unique", "TMs"]),
|
||||
"TM10 Double Edge": ItemData(210, ItemClassification.useful, ["Unique", "TMs"]),
|
||||
"TM11 Bubble Beam": ItemData(211, ItemClassification.useful, ["Unique", "TMs"]),
|
||||
"TM12 Water Gun": ItemData(212, ItemClassification.useful, ["Unique", "TMs"]),
|
||||
"TM13 Ice Beam": ItemData(213, ItemClassification.useful, ["Unique", "TMs"]),
|
||||
"TM14 Blizzard": ItemData(214, ItemClassification.useful, ["Unique", "TMs"]),
|
||||
"TM15 Hyper Beam": ItemData(215, ItemClassification.useful, ["Unique", "TMs"]),
|
||||
"TM16 Pay Day": ItemData(216, ItemClassification.useful, ["Unique", "TMs"]),
|
||||
"TM17 Submission": ItemData(217, ItemClassification.useful, ["Unique", "TMs"]),
|
||||
"TM18 Counter": ItemData(218, ItemClassification.filler, ["Unique", "TMs"]),
|
||||
"TM19 Seismic Toss": ItemData(219, ItemClassification.useful, ["Unique", "TMs"]),
|
||||
"TM20 Rage": ItemData(220, ItemClassification.useful, ["Unique", "TMs"]),
|
||||
"TM21 Mega Drain": ItemData(221, ItemClassification.useful, ["Unique", "TMs"]),
|
||||
"TM22 Solar Beam": ItemData(222, ItemClassification.useful, ["Unique", "TMs"]),
|
||||
"TM23 Dragon Rage": ItemData(223, ItemClassification.useful, ["Unique", "TMs"]),
|
||||
"TM24 Thunderbolt": ItemData(224, ItemClassification.useful, ["Unique", "TMs"]),
|
||||
"TM25 Thunder": ItemData(225, ItemClassification.useful, ["Unique", "TMs"]),
|
||||
"TM26 Earthquake": ItemData(226, ItemClassification.useful, ["Unique", "TMs"]),
|
||||
"TM27 Fissure": ItemData(227, ItemClassification.useful, ["Unique", "TMs"]),
|
||||
"TM28 Dig": ItemData(228, ItemClassification.useful, ["Unique", "TMs"]),
|
||||
"TM29 Psychic": ItemData(229, ItemClassification.useful, ["Unique", "TMs"]),
|
||||
"TM30 Teleport": ItemData(230, ItemClassification.filler, ["Unique", "TMs"]),
|
||||
"TM31 Mimic": ItemData(231, ItemClassification.useful, ["Unique", "TMs"]),
|
||||
"TM32 Double Team": ItemData(232, ItemClassification.useful, ["Unique", "TMs"]),
|
||||
"TM33 Reflect": ItemData(233, ItemClassification.useful, ["Unique", "TMs"]),
|
||||
"TM34 Bide": ItemData(234, ItemClassification.filler, ["Unique", "TMs"]),
|
||||
"TM35 Metronome": ItemData(235, ItemClassification.useful, ["Unique", "TMs"]),
|
||||
"TM36 Self Destruct": ItemData(236, ItemClassification.useful, ["Unique", "TMs"]),
|
||||
"TM37 Egg Bomb": ItemData(237, ItemClassification.useful, ["Unique", "TMs"]),
|
||||
"TM38 Fire Blast": ItemData(238, ItemClassification.useful, ["Unique", "TMs"]),
|
||||
"TM39 Swift": ItemData(239, ItemClassification.useful, ["Unique", "TMs"]),
|
||||
"TM40 Skull Bash": ItemData(240, ItemClassification.filler, ["Unique", "TMs"]),
|
||||
"TM41 Soft Boiled": ItemData(241, ItemClassification.useful, ["Unique", "TMs"]),
|
||||
"TM42 Dream Eater": ItemData(242, ItemClassification.useful, ["Unique", "TMs"]),
|
||||
"TM43 Sky Attack": ItemData(243, ItemClassification.filler, ["Unique", "TMs"]),
|
||||
"TM44 Rest": ItemData(244, ItemClassification.useful, ["Unique", "TMs"]),
|
||||
"TM45 Thunder Wave": ItemData(245, ItemClassification.useful, ["Unique", "TMs"]),
|
||||
"TM46 Psywave": ItemData(246, ItemClassification.filler, ["Unique", "TMs"]),
|
||||
"TM47 Explosion": ItemData(247, ItemClassification.useful, ["Unique", "TMs"]),
|
||||
"TM48 Rock Slide": ItemData(248, ItemClassification.useful, ["Unique", "TMs"]),
|
||||
"TM49 Tri Attack": ItemData(249, ItemClassification.useful, ["Unique", "TMs"]),
|
||||
"TM50 Substitute": ItemData(250, ItemClassification.useful, ["Unique", "TMs"]),
|
||||
|
||||
"Fuji Saved": ItemData(None, ItemClassification.progression, []),
|
||||
"Silph Co Liberated": ItemData(None, ItemClassification.progression, []),
|
||||
"Become Champion": ItemData(None, ItemClassification.progression, [])
|
||||
}
|
||||
item_table.update(
|
||||
{pokemon: ItemData(None, ItemClassification.progression, []) for pokemon in pokemon_data.keys()}
|
||||
)
|
||||
item_table.update(
|
||||
{f"Missable {pokemon}": ItemData(None, ItemClassification.useful, []) for pokemon in pokemon_data.keys()}
|
||||
)
|
||||
item_table.update(
|
||||
{f"Static {pokemon}": ItemData(None, ItemClassification.progression, []) for pokemon in pokemon_data.keys()}
|
||||
)
|
||||
|
||||
|
||||
item_groups = {}
|
||||
for item, data in item_table.items():
|
||||
for group in data.groups:
|
||||
item_groups[group] = item_groups.get(group, []) + [item]
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,73 @@
|
||||
from ..AutoWorld import LogicMixin
|
||||
import worlds.pokemon_rb.poke_data as poke_data
|
||||
|
||||
|
||||
class PokemonLogic(LogicMixin):
|
||||
def pokemon_rb_can_surf(self, player):
|
||||
return (((self.has("HM03 Surf", player) and self.can_learn_hm("10000", player))
|
||||
or self.has("Flippers", player)) and (self.has("Soul Badge", player) or
|
||||
self.has(self.world.worlds[player].extra_badges.get("Surf"), player)
|
||||
or self.world.badges_needed_for_hm_moves[player].value == 0))
|
||||
|
||||
def pokemon_rb_can_cut(self, player):
|
||||
return ((self.has("HM01 Cut", player) and self.can_learn_hm("100", player) or self.has("Master Sword", player))
|
||||
and (self.has("Cascade Badge", player) or
|
||||
self.has(self.world.worlds[player].extra_badges.get("Cut"), player) or
|
||||
self.world.badges_needed_for_hm_moves[player].value == 0))
|
||||
|
||||
def pokemon_rb_can_fly(self, player):
|
||||
return (((self.has("HM02 Fly", player) and self.can_learn_hm("1000", player)) or self.has("Flute", player)) and
|
||||
(self.has("Thunder Badge", player) or self.has(self.world.worlds[player].extra_badges.get("Fly"), player)
|
||||
or self.world.badges_needed_for_hm_moves[player].value == 0))
|
||||
|
||||
def pokemon_rb_can_strength(self, player):
|
||||
return ((self.has("HM04 Strength", player) and self.can_learn_hm("100000", player)) or
|
||||
self.has("Titan's Mitt", player)) and (self.has("Rainbow Badge", player) or
|
||||
self.has(self.world.worlds[player].extra_badges.get("Strength"), player)
|
||||
or self.world.badges_needed_for_hm_moves[player].value == 0)
|
||||
|
||||
def pokemon_rb_can_flash(self, player):
|
||||
return (((self.has("HM05 Flash", player) and self.can_learn_hm("1000000", player)) or self.has("Lamp", player))
|
||||
and (self.has("Boulder Badge", player) or self.has(self.world.worlds[player].extra_badges.get("Flash"),
|
||||
player) or self.world.badges_needed_for_hm_moves[player].value == 0))
|
||||
|
||||
def can_learn_hm(self, move, player):
|
||||
for pokemon, data in self.world.worlds[player].local_poke_data.items():
|
||||
if self.has(pokemon, player) and data["tms"][6] & int(move, 2):
|
||||
return True
|
||||
return False
|
||||
|
||||
def pokemon_rb_can_get_hidden_items(self, player):
|
||||
return self.has("Item Finder", player) or not self.world.require_item_finder[player].value
|
||||
|
||||
def pokemon_rb_cerulean_cave(self, count, player):
|
||||
return len([item for item in
|
||||
["Boulder Badge", "Cascade Badge", "Thunder Badge", "Rainbow Badge", "Soul Badge", "Marsh Badge",
|
||||
"Volcano Badge", "Earth Badge", "Bicycle", "Silph Scope", "Item Finder", "Super Rod", "Good Rod",
|
||||
"Old Rod", "Lift Key", "Card Key", "Town Map", "Coin Case", "S.S. Ticket", "Secret Key",
|
||||
"Mansion Key", "Safari Pass", "Plant Key", "Hideout Key", "HM01 Cut", "HM02 Fly", "HM03 Surf",
|
||||
"HM04 Strength", "HM05 Flash"] if self.has(item, player)]) >= count
|
||||
|
||||
def pokemon_rb_can_pass_guards(self, player):
|
||||
if self.world.tea[player].value:
|
||||
return self.has("Tea", player)
|
||||
else:
|
||||
# this could just be "True", but you never know what weird options I might introduce later ;)
|
||||
return self.can_reach("Celadon City - Counter Man", "Location", player)
|
||||
|
||||
def pokemon_rb_has_badges(self, count, player):
|
||||
return len([item for item in ["Boulder Badge", "Cascade Badge", "Thunder Badge", "Rainbow Badge", "Marsh Badge",
|
||||
"Soul Badge", "Volcano Badge", "Earth Badge"] if self.has(item, player)]) >= count
|
||||
|
||||
def pokemon_rb_has_pokemon(self, count, player):
|
||||
obtained_pokemon = set()
|
||||
for pokemon in poke_data.pokemon_data.keys():
|
||||
if self.has(pokemon, player) or self.has(f"Static {pokemon}", player):
|
||||
obtained_pokemon.add(pokemon)
|
||||
|
||||
return len(obtained_pokemon) >= count
|
||||
|
||||
def pokemon_rb_fossil_checks(self, count, player):
|
||||
return (self.can_reach('Mt Moon 1F - Southwest Item', 'Location', player) and
|
||||
self.can_reach('Cinnabar Island - Lab Scientist', 'Location', player) and len(
|
||||
[item for item in ["Dome Fossil", "Helix Fossil", "Old Amber"] if self.has(item, player)]) >= count)
|
||||
@@ -0,0 +1,480 @@
|
||||
|
||||
from Options import Toggle, Choice, Range, SpecialRange, FreeText, TextChoice
|
||||
|
||||
|
||||
class GameVersion(Choice):
|
||||
"""Select Red or Blue version."""
|
||||
display_name = "Game Version"
|
||||
option_red = 1
|
||||
option_blue = 0
|
||||
default = "random"
|
||||
|
||||
|
||||
class TrainerName(FreeText):
|
||||
"""Your trainer name. Cannot exceed 7 characters.
|
||||
See the setup guide on archipelago.gg for a list of allowed characters."""
|
||||
display_name = "Trainer Name"
|
||||
default = "ASH"
|
||||
|
||||
|
||||
class RivalName(FreeText):
|
||||
"""Your rival's name. Cannot exceed 7 characters.
|
||||
See the setup guide on archipelago.gg for a list of allowed characters."""
|
||||
display_name = "Rival's Name"
|
||||
default = "GARY"
|
||||
|
||||
|
||||
class Goal(Choice):
|
||||
"""If Professor Oak is selected, your victory condition will require challenging and defeating Oak after becoming"""
|
||||
"""Champion and defeating or capturing the Pokemon at the end of Cerulean Cave."""
|
||||
display_name = "Goal"
|
||||
option_pokemon_league = 0
|
||||
option_professor_oak = 1
|
||||
default = 0
|
||||
|
||||
|
||||
class EliteFourCondition(Range):
|
||||
"""Number of badges required to challenge the Elite Four once the Indigo Plateau has been reached.
|
||||
Your rival will reveal the amount needed on the first Route 22 battle (after turning in Oak's Parcel)."""
|
||||
display_name = "Elite Four Condition"
|
||||
range_start = 0
|
||||
range_end = 8
|
||||
default = 8
|
||||
|
||||
|
||||
class VictoryRoadCondition(Range):
|
||||
"""Number of badges required to reach Victory Road."""
|
||||
display_name = "Victory Road Condition"
|
||||
range_start = 0
|
||||
range_end = 8
|
||||
default = 8
|
||||
|
||||
|
||||
class ViridianGymCondition(Range):
|
||||
"""Number of badges required to enter Viridian Gym."""
|
||||
display_name = "Viridian Gym Condition"
|
||||
range_start = 0
|
||||
range_end = 7
|
||||
default = 7
|
||||
|
||||
|
||||
class CeruleanCaveCondition(Range):
|
||||
"""Number of badges, HMs, and key items (not counting items you can lose) required to access Cerulean Cave."""
|
||||
"""If extra_key_items is turned on, the number chosen will be increased by 4."""
|
||||
display_name = "Cerulean Cave Condition"
|
||||
range_start = 0
|
||||
range_end = 25
|
||||
default = 20
|
||||
|
||||
|
||||
class SecondFossilCheckCondition(Range):
|
||||
"""After choosing one of the fossil location items, you can obtain the remaining item from the Cinnabar Lab
|
||||
Scientist after reviving this number of fossils."""
|
||||
display_name = "Second Fossil Check Condition"
|
||||
range_start = 0
|
||||
range_end = 3
|
||||
default = 3
|
||||
|
||||
|
||||
class BadgeSanity(Toggle):
|
||||
"""Shuffle gym badges into the general item pool. If turned off, badges will be shuffled across the 8 gyms."""
|
||||
display_name = "Badgesanity"
|
||||
default = 0
|
||||
|
||||
|
||||
class BadgesNeededForHMMoves(Choice):
|
||||
"""Off will remove the requirement for badges to use HM moves. Extra will give the Marsh, Volcano, and Earth
|
||||
Badges a random HM move to enable. Extra Plus will additionally pick two random badges to enable a second HM move.
|
||||
A man in Cerulean City will reveal the moves enabled by each Badge."""
|
||||
display_name = "Badges Needed For HM Moves"
|
||||
default = 1
|
||||
option_on = 1
|
||||
alias_true = 1
|
||||
option_off = 0
|
||||
alias_false = 0
|
||||
option_extra = 2
|
||||
option_extra_plus = 3
|
||||
|
||||
|
||||
class OldMan(Choice):
|
||||
"""With Open Viridian City, the Old Man will let you through without needing to turn in Oak's Parcel."""
|
||||
"""Early Parcel will ensure Oak's Parcel is available at the beginning of your game."""
|
||||
display_name = "Old Man"
|
||||
option_vanilla = 0
|
||||
option_early_parcel = 1
|
||||
option_open_viridian_city = 2
|
||||
default = 1
|
||||
|
||||
|
||||
class Tea(Toggle):
|
||||
"""Adds a Tea item to the item pool which the Saffron guards require instead of the vending machine drinks.
|
||||
Adds a location check to the Celadon Mansion 1F, where Tea is acquired in FireRed and LeafGreen."""
|
||||
display_name = "Tea"
|
||||
default = 0
|
||||
|
||||
|
||||
class ExtraKeyItems(Toggle):
|
||||
"""Adds key items that are required to access the Rocket Hideout, Cinnabar Mansion, Safari Zone, and Power Plant.
|
||||
Adds four item pickups to Rock Tunnel B1F."""
|
||||
display_name = "Extra Key Items"
|
||||
default = 0
|
||||
|
||||
|
||||
class ExtraStrengthBoulders(Toggle):
|
||||
"""Adds Strength Boulders blocking the Route 11 gate, and in Route 13 (can be bypassed with Surf).
|
||||
This potentially increases the usefulness of Strength as well as the Bicycle."""
|
||||
display_name = "Extra Strength Boulders"
|
||||
default = 0
|
||||
|
||||
|
||||
class RequireItemFinder(Toggle):
|
||||
"""Require Item Finder to pick up hidden items."""
|
||||
display_name = "Require Item Finder"
|
||||
default = 0
|
||||
|
||||
|
||||
class RandomizeHiddenItems(Choice):
|
||||
"""Randomize hidden items. If you choose exclude, they will be randomized but will be guaranteed junk items."""
|
||||
display_name = "Randomize Hidden Items"
|
||||
option_on = 1
|
||||
option_off = 0
|
||||
alias_true = 1
|
||||
alias_false = 0
|
||||
option_exclude = 2
|
||||
default = 0
|
||||
|
||||
|
||||
class FreeFlyLocation(Toggle):
|
||||
"""One random fly destination will be unlocked by default."""
|
||||
display_name = "Free Fly Location"
|
||||
default = 1
|
||||
|
||||
|
||||
class OaksAidRt2(Range):
|
||||
"""Number of Pokemon registered in the Pokedex required to receive the item from Oak's Aide on Route 2"""
|
||||
display_name = "Oak's Aide Route 2"
|
||||
range_start = 0
|
||||
range_end = 80
|
||||
default = 10
|
||||
|
||||
|
||||
class OaksAidRt11(Range):
|
||||
"""Number of Pokemon registered in the Pokedex required to receive the item from Oak's Aide on Route 11"""
|
||||
display_name = "Oak's Aide Route 11"
|
||||
range_start = 0
|
||||
range_end = 80
|
||||
default = 30
|
||||
|
||||
|
||||
class OaksAidRt15(Range):
|
||||
"""Number of Pokemon registered in the Pokedex required to receive the item from Oak's Aide on Route 15"""
|
||||
display_name = "Oak's Aide Route 15"
|
||||
range_start = 0
|
||||
range_end = 80
|
||||
default = 50
|
||||
|
||||
|
||||
class ExpModifier(SpecialRange):
|
||||
"""Modifier for EXP gained. When specifying a number, exp is multiplied by this amount and divided by 16."""
|
||||
display_name = "Exp Modifier"
|
||||
range_start = 0
|
||||
range_end = 255
|
||||
default = 16
|
||||
special_range_names = {
|
||||
"half": default / 2,
|
||||
"normal": default,
|
||||
"double": default * 2,
|
||||
"triple": default * 3,
|
||||
"quadruple": default * 4,
|
||||
"quintuple": default * 5,
|
||||
"sextuple": default * 6,
|
||||
"septuple": default * 7,
|
||||
"octuple": default * 8,
|
||||
}
|
||||
|
||||
|
||||
class RandomizeWildPokemon(Choice):
|
||||
"""Randomize all wild Pokemon and game corner prize Pokemon. match_types will select a Pokemon with at least one
|
||||
type matching the original type of the original Pokemon. match_base_stats will prefer Pokemon with closer base stat
|
||||
totals. match_types_and_base_stats will match types and will weight towards similar base stats, but there may not be
|
||||
many to choose from."""
|
||||
display_name = "Randomize Wild Pokemon"
|
||||
default = 0
|
||||
option_vanilla = 0
|
||||
option_match_types = 1
|
||||
option_match_base_stats = 2
|
||||
option_match_types_and_base_stats = 3
|
||||
option_completely_random = 4
|
||||
|
||||
|
||||
class RandomizeStarterPokemon(Choice):
|
||||
"""Randomize the starter Pokemon choices."""
|
||||
display_name = "Randomize Starter Pokemon"
|
||||
default = 0
|
||||
option_vanilla = 0
|
||||
option_match_types = 1
|
||||
option_match_base_stats = 2
|
||||
option_match_types_and_base_stats = 3
|
||||
option_completely_random = 4
|
||||
|
||||
|
||||
class RandomizeStaticPokemon(Choice):
|
||||
"""Randomize one-time gift and encountered Pokemon. These will always be first evolution stage Pokemon."""
|
||||
display_name = "Randomize Static Pokemon"
|
||||
default = 0
|
||||
option_vanilla = 0
|
||||
option_match_types = 1
|
||||
option_match_base_stats = 2
|
||||
option_match_types_and_base_stats = 3
|
||||
option_completely_random = 4
|
||||
|
||||
|
||||
class RandomizeLegendaryPokemon(Choice):
|
||||
"""Randomize Legendaries. Mew has been added as an encounter at the Vermilion dock truck.
|
||||
Shuffle will shuffle the legendaries with each other. Static will shuffle them into other static Pokemon locations.
|
||||
'Any' will allow legendaries to appear anywhere based on wild and static randomization options, and their locations
|
||||
will be randomized according to static Pokemon randomization options."""
|
||||
display_name = "Randomize Legendary Pokemon"
|
||||
default = 0
|
||||
option_vanilla = 0
|
||||
option_shuffle = 1
|
||||
option_static = 2
|
||||
option_any = 3
|
||||
|
||||
|
||||
class CatchEmAll(Choice):
|
||||
"""Guarantee all first evolution stage Pokemon are available, or all Pokemon of all stages.
|
||||
Currently only has an effect if wild Pokemon are randomized."""
|
||||
display_name = "Catch 'Em All"
|
||||
default = 0
|
||||
option_off = 0
|
||||
alias_false = 0
|
||||
option_first_stage = 1
|
||||
option_all_pokemon = 2
|
||||
|
||||
|
||||
class RandomizeTrainerParties(Choice):
|
||||
"""Randomize enemy Pokemon encountered in trainer battles."""
|
||||
display_name = "Randomize Trainer Parties"
|
||||
default = 0
|
||||
option_vanilla = 0
|
||||
option_match_types = 1
|
||||
option_match_base_stats = 2
|
||||
option_match_types_and_base_stats = 3
|
||||
option_completely_random = 4
|
||||
|
||||
|
||||
class TrainerLegendaries(Toggle):
|
||||
"""Allow legendary Pokemon in randomized trainer parties."""
|
||||
display_name = "Trainer Legendaries"
|
||||
default = 0
|
||||
|
||||
|
||||
class BlindTrainers(Range):
|
||||
"""Chance each frame that you are standing on a tile in a trainer's line of sight that they will fail to initiate a
|
||||
battle. If you move into and out of their line of sight without stopping, this chance will only trigger once."""
|
||||
display_name = "Blind Trainers"
|
||||
range_start = 0
|
||||
range_end = 100
|
||||
default = 0
|
||||
|
||||
|
||||
class MinimumStepsBetweenEncounters(Range):
|
||||
"""Minimum number of steps between wild Pokemon encounters."""
|
||||
display_name = "Minimum Steps Between Encounters"
|
||||
default = 3
|
||||
range_start = 0
|
||||
range_end = 255
|
||||
|
||||
|
||||
class RandomizePokemonStats(Choice):
|
||||
"""Randomize base stats for each Pokemon. Shuffle will shuffle the 5 base stat values amongst each other. Randomize
|
||||
will completely randomize each stat, but will still add up to the same base stat total."""
|
||||
display_name = "Randomize Pokemon Stats"
|
||||
default = 0
|
||||
option_vanilla = 0
|
||||
option_shuffle = 1
|
||||
option_randomize = 2
|
||||
|
||||
|
||||
class RandomizePokemonCatchRates(Toggle):
|
||||
"""Randomize the catch rate for each Pokemon."""
|
||||
display_name = "Randomize Catch Rates"
|
||||
default = 0
|
||||
|
||||
|
||||
class MinimumCatchRate(Range):
|
||||
"""Minimum catch rate for each Pokemon. If randomize_catch_rates is on, this will be the minimum value that can be
|
||||
chosen. Otherwise, it will raise any Pokemon's catch rate up to this value if its normal catch rate is lower."""
|
||||
display_name = "Minimum Catch Rate"
|
||||
range_start = 1
|
||||
range_end = 255
|
||||
default = 3
|
||||
|
||||
|
||||
class RandomizePokemonMovesets(Choice):
|
||||
"""Randomize the moves learned by Pokemon. prefer_types will prefer moves that match the type of the Pokemon."""
|
||||
display_name = "Randomize Pokemon Movesets"
|
||||
option_vanilla = 0
|
||||
option_prefer_types = 1
|
||||
option_completely_random = 2
|
||||
default = 0
|
||||
|
||||
|
||||
class StartWithFourMoves(Toggle):
|
||||
"""If movesets are randomized, this will give all Pokemon 4 starting moves."""
|
||||
display_name = "Start With Four Moves"
|
||||
default = 0
|
||||
|
||||
|
||||
class TMCompatibility(Choice):
|
||||
"""Randomize which Pokemon can learn each TM. prefer_types: 90% chance if Pokemon's type matches the move,
|
||||
50% chance if move is Normal type and the Pokemon is not, and 25% chance otherwise. Pokemon will retain the same
|
||||
TM compatibility when they evolve if the evolved form has the same type(s). Mew will always be able to learn
|
||||
every TM."""
|
||||
display_name = "TM Compatibility"
|
||||
default = 0
|
||||
option_vanilla = 0
|
||||
option_prefer_types = 1
|
||||
option_completely_random = 2
|
||||
option_full_compatibility = 3
|
||||
|
||||
|
||||
class HMCompatibility(Choice):
|
||||
"""Randomize which Pokemon can learn each HM. prefer_types: 100% chance if Pokemon's type matches the move,
|
||||
75% chance if move is Normal type and the Pokemon is not, and 25% chance otherwise. Pokemon will retain the same
|
||||
HM compatibility when they evolve if the evolved form has the same type(s). Mew will always be able to learn
|
||||
every HM."""
|
||||
display_name = "HM Compatibility"
|
||||
default = 0
|
||||
option_vanilla = 0
|
||||
option_prefer_types = 1
|
||||
option_completely_random = 2
|
||||
option_full_compatibility = 3
|
||||
|
||||
|
||||
class RandomizePokemonTypes(Choice):
|
||||
"""Randomize the types of each Pokemon. Follow Evolutions will ensure Pokemon's types remain the same when evolving
|
||||
(except possibly gaining a type)."""
|
||||
display_name = "Pokemon Types"
|
||||
option_vanilla = 0
|
||||
option_follow_evolutions = 1
|
||||
option_randomize = 2
|
||||
default = 0
|
||||
|
||||
|
||||
class SecondaryTypeChance(SpecialRange):
|
||||
"""If randomize_pokemon_types is on, this is the chance each Pokemon will have a secondary type. If follow_evolutions
|
||||
is selected, it is the chance a second type will be added at each evolution stage. vanilla will give secondary types
|
||||
to Pokemon that normally have a secondary type."""
|
||||
display_name = "Secondary Type Chance"
|
||||
range_start = -1
|
||||
range_end = 100
|
||||
default = -1
|
||||
special_range_names = {
|
||||
"vanilla": -1
|
||||
}
|
||||
|
||||
|
||||
class RandomizeTypeChartTypes(Choice):
|
||||
"""The game's type chart consists of 3 columns: attacking type, defending type, and type effectiveness.
|
||||
Matchups that have regular type effectiveness are not in the chart. Shuffle will shuffle the attacking types
|
||||
across the attacking type column and the defending types across the defending type column (so for example Normal
|
||||
type will still have exactly 2 types that it receives non-regular damage from, and 2 types it deals non-regular
|
||||
damage to). Randomize will randomize each type in both columns to any random type."""
|
||||
display_name = "Randomize Type Chart Types"
|
||||
option_vanilla = 0
|
||||
option_shuffle = 1
|
||||
option_randomize = 2
|
||||
default = 0
|
||||
|
||||
|
||||
class RandomizeTypeChartTypeEffectiveness(Choice):
|
||||
"""The game's type chart consists of 3 columns: attacking type, defending type, and type effectiveness.
|
||||
Matchups that have regular type effectiveness are not in the chart. Shuffle will shuffle the type effectiveness
|
||||
across the type effectiveness column (so for example there will always be 6 type immunities). Randomize will
|
||||
randomize each entry in the table to no effect, not very effective, or super effective; with no effect occurring
|
||||
at a low chance. Chaos will randomize the values to anywhere between 0% and 200% damage, in 10% increments."""
|
||||
display_name = "Randomize Type Chart Type Effectiveness"
|
||||
option_vanilla = 0
|
||||
option_shuffle = 1
|
||||
option_randomize = 2
|
||||
option_chaos = 3
|
||||
default = 0
|
||||
|
||||
|
||||
class SafariZoneNormalBattles(Toggle):
|
||||
"""Change the Safari Zone to have standard wild pokemon battles."""
|
||||
display_name = "Safari Zone Normal Battles"
|
||||
default = 0
|
||||
|
||||
|
||||
class NormalizeEncounterChances(Toggle):
|
||||
"""Each wild encounter table has 10 slots for Pokemon. Normally the chance for each being chosen ranges from
|
||||
19.9% to 1.2%. Turn this on to normalize them all to 10% each."""
|
||||
display_name = "Normalize Encounter Chances"
|
||||
default = 0
|
||||
|
||||
|
||||
class ReusableTMs(Toggle):
|
||||
"""Makes TMs reusable, so they will not be consumed upon use."""
|
||||
display_name = "Reusable TMs"
|
||||
default = 0
|
||||
|
||||
|
||||
class StartingMoney(Range):
|
||||
"""The amount of money you start with."""
|
||||
display_name = "Starting Money"
|
||||
default = 3000
|
||||
range_start = 0
|
||||
range_end = 999999
|
||||
|
||||
|
||||
pokemon_rb_options = {
|
||||
"game_version": GameVersion,
|
||||
"trainer_name": TrainerName,
|
||||
"rival_name": RivalName,
|
||||
#"goal": Goal,
|
||||
"elite_four_condition": EliteFourCondition,
|
||||
"victory_road_condition": VictoryRoadCondition,
|
||||
"viridian_gym_condition": ViridianGymCondition,
|
||||
"cerulean_cave_condition": CeruleanCaveCondition,
|
||||
"second_fossil_check_condition": SecondFossilCheckCondition,
|
||||
"badgesanity": BadgeSanity,
|
||||
"old_man": OldMan,
|
||||
"tea": Tea,
|
||||
"extra_key_items": ExtraKeyItems,
|
||||
"extra_strength_boulders": ExtraStrengthBoulders,
|
||||
"require_item_finder": RequireItemFinder,
|
||||
"randomize_hidden_items": RandomizeHiddenItems,
|
||||
"badges_needed_for_hm_moves": BadgesNeededForHMMoves,
|
||||
"free_fly_location": FreeFlyLocation,
|
||||
"oaks_aide_rt_2": OaksAidRt2,
|
||||
"oaks_aide_rt_11": OaksAidRt11,
|
||||
"oaks_aide_rt_15": OaksAidRt15,
|
||||
"blind_trainers": BlindTrainers,
|
||||
"minimum_steps_between_encounters": MinimumStepsBetweenEncounters,
|
||||
"exp_modifier": ExpModifier,
|
||||
"randomize_wild_pokemon": RandomizeWildPokemon,
|
||||
"randomize_starter_pokemon": RandomizeStarterPokemon,
|
||||
"randomize_static_pokemon": RandomizeStaticPokemon,
|
||||
"randomize_legendary_pokemon": RandomizeLegendaryPokemon,
|
||||
"catch_em_all": CatchEmAll,
|
||||
"randomize_pokemon_stats": RandomizePokemonStats,
|
||||
"randomize_pokemon_catch_rates": RandomizePokemonCatchRates,
|
||||
"minimum_catch_rate": MinimumCatchRate,
|
||||
"randomize_trainer_parties": RandomizeTrainerParties,
|
||||
"trainer_legendaries": TrainerLegendaries,
|
||||
"randomize_pokemon_movesets": RandomizePokemonMovesets,
|
||||
"start_with_four_moves": StartWithFourMoves,
|
||||
"tm_compatibility": TMCompatibility,
|
||||
"hm_compatibility": HMCompatibility,
|
||||
"randomize_pokemon_types": RandomizePokemonTypes,
|
||||
"secondary_type_chance": SecondaryTypeChance,
|
||||
"randomize_type_matchup_types": RandomizeTypeChartTypes,
|
||||
"randomize_type_matchup_type_effectiveness": RandomizeTypeChartTypeEffectiveness,
|
||||
"safari_zone_normal_battles": SafariZoneNormalBattles,
|
||||
"normalize_encounter_chances": NormalizeEncounterChances,
|
||||
"reusable_tms": ReusableTMs,
|
||||
"starting_money": StartingMoney,
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
@@ -0,0 +1,305 @@
|
||||
|
||||
from BaseClasses import MultiWorld, Region, Entrance, RegionType, LocationProgressType
|
||||
from worlds.generic.Rules import add_item_rule
|
||||
from .locations import location_data, PokemonRBLocation
|
||||
|
||||
|
||||
def create_region(world: MultiWorld, player: int, name: str, locations_per_region=None, exits=None):
|
||||
ret = Region(name, RegionType.Generic, name, player, world)
|
||||
for location in locations_per_region.get(name, []):
|
||||
if (world.randomize_hidden_items[player].value or "Hidden" not in location.name) and \
|
||||
(world.extra_key_items[player].value or name != "Rock Tunnel B1F" or "Item" not in location.name) and \
|
||||
(world.tea[player].value or location.name != "Celadon City - Mansion Lady"):
|
||||
location.parent_region = ret
|
||||
ret.locations.append(location)
|
||||
if world.randomize_hidden_items[player].value == 2 and "Hidden" in location.name:
|
||||
location.progress_type = LocationProgressType.EXCLUDED
|
||||
add_item_rule(location, lambda i: not (i.advancement or i.useful))
|
||||
if exits:
|
||||
for exit in exits:
|
||||
ret.exits.append(Entrance(player, exit, ret))
|
||||
locations_per_region[name] = []
|
||||
return ret
|
||||
|
||||
|
||||
def create_regions(world: MultiWorld, player: int):
|
||||
locations_per_region = {}
|
||||
for location in location_data:
|
||||
locations_per_region.setdefault(location.region, [])
|
||||
locations_per_region[location.region].append(PokemonRBLocation(player, location.name, location.address,
|
||||
location.rom_address))
|
||||
regions = [
|
||||
create_region(world, player, "Menu", locations_per_region),
|
||||
create_region(world, player, "Anywhere", locations_per_region),
|
||||
create_region(world, player, "Fossil", locations_per_region),
|
||||
create_region(world, player, "Pallet Town", locations_per_region),
|
||||
create_region(world, player, "Route 1", locations_per_region),
|
||||
create_region(world, player, "Viridian City", locations_per_region),
|
||||
create_region(world, player, "Viridian City North", locations_per_region),
|
||||
create_region(world, player, "Viridian Gym", locations_per_region),
|
||||
create_region(world, player, "Route 2", locations_per_region),
|
||||
create_region(world, player, "Route 2 East", locations_per_region),
|
||||
create_region(world, player, "Diglett's Cave", locations_per_region),
|
||||
create_region(world, player, "Route 22", locations_per_region),
|
||||
create_region(world, player, "Route 23 South", locations_per_region),
|
||||
create_region(world, player, "Route 23 North", locations_per_region),
|
||||
create_region(world, player, "Viridian Forest", locations_per_region),
|
||||
create_region(world, player, "Pewter City", locations_per_region),
|
||||
create_region(world, player, "Pewter Gym", locations_per_region),
|
||||
create_region(world, player, "Route 3", locations_per_region),
|
||||
create_region(world, player, "Mt Moon 1F", locations_per_region),
|
||||
create_region(world, player, "Mt Moon B1F", locations_per_region),
|
||||
create_region(world, player, "Mt Moon B2F", locations_per_region),
|
||||
create_region(world, player, "Route 4", locations_per_region),
|
||||
create_region(world, player, "Cerulean City", locations_per_region),
|
||||
create_region(world, player, "Cerulean Gym", locations_per_region),
|
||||
create_region(world, player, "Route 24", locations_per_region),
|
||||
create_region(world, player, "Route 25", locations_per_region),
|
||||
create_region(world, player, "Route 9", locations_per_region),
|
||||
create_region(world, player, "Route 10 North", locations_per_region),
|
||||
create_region(world, player, "Rock Tunnel 1F", locations_per_region),
|
||||
create_region(world, player, "Rock Tunnel B1F", locations_per_region),
|
||||
create_region(world, player, "Power Plant", locations_per_region),
|
||||
create_region(world, player, "Route 10 South", locations_per_region),
|
||||
create_region(world, player, "Lavender Town", locations_per_region),
|
||||
create_region(world, player, "Pokemon Tower 1F", locations_per_region),
|
||||
create_region(world, player, "Pokemon Tower 2F", locations_per_region),
|
||||
create_region(world, player, "Pokemon Tower 3F", locations_per_region),
|
||||
create_region(world, player, "Pokemon Tower 4F", locations_per_region),
|
||||
create_region(world, player, "Pokemon Tower 5F", locations_per_region),
|
||||
create_region(world, player, "Pokemon Tower 6F", locations_per_region),
|
||||
create_region(world, player, "Pokemon Tower 7F", locations_per_region),
|
||||
create_region(world, player, "Route 5", locations_per_region),
|
||||
create_region(world, player, "Saffron City", locations_per_region),
|
||||
create_region(world, player, "Saffron Gym", locations_per_region),
|
||||
create_region(world, player, "Copycat's House", locations_per_region),
|
||||
create_region(world, player, "Underground Tunnel North-South", locations_per_region),
|
||||
create_region(world, player, "Route 6", locations_per_region),
|
||||
create_region(world, player, "Vermilion City", locations_per_region),
|
||||
create_region(world, player, "Vermilion Gym", locations_per_region),
|
||||
create_region(world, player, "S.S. Anne 1F", locations_per_region),
|
||||
create_region(world, player, "S.S. Anne B1F", locations_per_region),
|
||||
create_region(world, player, "S.S. Anne 2F", locations_per_region),
|
||||
create_region(world, player, "Route 11", locations_per_region),
|
||||
create_region(world, player, "Route 11 East", locations_per_region),
|
||||
create_region(world, player, "Route 12 North", locations_per_region),
|
||||
create_region(world, player, "Route 12 South", locations_per_region),
|
||||
create_region(world, player, "Route 12 Grass", locations_per_region),
|
||||
create_region(world, player, "Route 12 West", locations_per_region),
|
||||
create_region(world, player, "Route 7", locations_per_region),
|
||||
create_region(world, player, "Underground Tunnel West-East", locations_per_region),
|
||||
create_region(world, player, "Route 8", locations_per_region),
|
||||
create_region(world, player, "Route 8 Grass", locations_per_region),
|
||||
create_region(world, player, "Celadon City", locations_per_region),
|
||||
create_region(world, player, "Celadon Prize Corner", locations_per_region),
|
||||
create_region(world, player, "Celadon Gym", locations_per_region),
|
||||
create_region(world, player, "Route 16", locations_per_region),
|
||||
create_region(world, player, "Route 16 North", locations_per_region),
|
||||
create_region(world, player, "Route 17", locations_per_region),
|
||||
create_region(world, player, "Route 18", locations_per_region),
|
||||
create_region(world, player, "Fuchsia City", locations_per_region),
|
||||
create_region(world, player, "Fuchsia Gym", locations_per_region),
|
||||
create_region(world, player, "Safari Zone Gate", locations_per_region),
|
||||
create_region(world, player, "Safari Zone Center", locations_per_region),
|
||||
create_region(world, player, "Safari Zone East", locations_per_region),
|
||||
create_region(world, player, "Safari Zone North", locations_per_region),
|
||||
create_region(world, player, "Safari Zone West", locations_per_region),
|
||||
create_region(world, player, "Route 15", locations_per_region),
|
||||
create_region(world, player, "Route 14", locations_per_region),
|
||||
create_region(world, player, "Route 13", locations_per_region),
|
||||
create_region(world, player, "Route 19", locations_per_region),
|
||||
create_region(world, player, "Route 20 East", locations_per_region),
|
||||
create_region(world, player, "Route 20 West", locations_per_region),
|
||||
create_region(world, player, "Seafoam Islands 1F", locations_per_region),
|
||||
create_region(world, player, "Seafoam Islands B1F", locations_per_region),
|
||||
create_region(world, player, "Seafoam Islands B2F", locations_per_region),
|
||||
create_region(world, player, "Seafoam Islands B3F", locations_per_region),
|
||||
create_region(world, player, "Seafoam Islands B4F", locations_per_region),
|
||||
create_region(world, player, "Cinnabar Island", locations_per_region),
|
||||
create_region(world, player, "Cinnabar Gym", locations_per_region),
|
||||
create_region(world, player, "Route 21", locations_per_region),
|
||||
create_region(world, player, "Silph Co 1F", locations_per_region),
|
||||
create_region(world, player, "Silph Co 2F", locations_per_region),
|
||||
create_region(world, player, "Silph Co 3F", locations_per_region),
|
||||
create_region(world, player, "Silph Co 4F", locations_per_region),
|
||||
create_region(world, player, "Silph Co 5F", locations_per_region),
|
||||
create_region(world, player, "Silph Co 6F", locations_per_region),
|
||||
create_region(world, player, "Silph Co 7F", locations_per_region),
|
||||
create_region(world, player, "Silph Co 8F", locations_per_region),
|
||||
create_region(world, player, "Silph Co 9F", locations_per_region),
|
||||
create_region(world, player, "Silph Co 10F", locations_per_region),
|
||||
create_region(world, player, "Silph Co 11F", locations_per_region),
|
||||
create_region(world, player, "Rocket Hideout B1F", locations_per_region),
|
||||
create_region(world, player, "Rocket Hideout B2F", locations_per_region),
|
||||
create_region(world, player, "Rocket Hideout B3F", locations_per_region),
|
||||
create_region(world, player, "Rocket Hideout B4F", locations_per_region),
|
||||
create_region(world, player, "Pokemon Mansion 1F", locations_per_region),
|
||||
create_region(world, player, "Pokemon Mansion 2F", locations_per_region),
|
||||
create_region(world, player, "Pokemon Mansion 3F", locations_per_region),
|
||||
create_region(world, player, "Pokemon Mansion B1F", locations_per_region),
|
||||
create_region(world, player, "Victory Road 1F", locations_per_region),
|
||||
create_region(world, player, "Victory Road 2F", locations_per_region),
|
||||
create_region(world, player, "Victory Road 3F", locations_per_region),
|
||||
create_region(world, player, "Indigo Plateau", locations_per_region),
|
||||
create_region(world, player, "Cerulean Cave 1F", locations_per_region),
|
||||
create_region(world, player, "Cerulean Cave 2F", locations_per_region),
|
||||
create_region(world, player, "Cerulean Cave B1F", locations_per_region),
|
||||
create_region(world, player, "Evolution", locations_per_region),
|
||||
]
|
||||
world.regions += regions
|
||||
connect(world, player, "Menu", "Anywhere", one_way=True)
|
||||
connect(world, player, "Menu", "Pallet Town", one_way=True)
|
||||
connect(world, player, "Menu", "Fossil", lambda state: state.pokemon_rb_fossil_checks(
|
||||
state.world.second_fossil_check_condition[player].value, player), one_way=True)
|
||||
connect(world, player, "Pallet Town", "Route 1")
|
||||
connect(world, player, "Route 1", "Viridian City")
|
||||
connect(world, player, "Viridian City", "Route 22")
|
||||
connect(world, player, "Route 22", "Route 23 South",
|
||||
lambda state: state.pokemon_rb_has_badges(state.world.victory_road_condition[player].value, player))
|
||||
connect(world, player, "Route 23 South", "Route 23 North", lambda state: state.pokemon_rb_can_surf(player))
|
||||
connect(world, player, "Viridian City North", "Viridian Gym", lambda state:
|
||||
state.pokemon_rb_has_badges(state.world.viridian_gym_condition[player].value, player), one_way=True)
|
||||
connect(world, player, "Route 2", "Route 2 East", lambda state: state.pokemon_rb_can_cut(player))
|
||||
connect(world, player, "Route 2 East", "Diglett's Cave", lambda state: state.pokemon_rb_can_cut(player))
|
||||
connect(world, player, "Route 2", "Viridian City North")
|
||||
connect(world, player, "Route 2", "Viridian Forest")
|
||||
connect(world, player, "Route 2", "Pewter City")
|
||||
connect(world, player, "Pewter City", "Pewter Gym", one_way=True)
|
||||
connect(world, player, "Pewter City", "Route 3")
|
||||
connect(world, player, "Route 4", "Route 3", one_way=True)
|
||||
connect(world, player, "Mt Moon 1F", "Mt Moon B1F", one_way=True)
|
||||
connect(world, player, "Mt Moon B1F", "Mt Moon B2F", one_way=True)
|
||||
connect(world, player, "Mt Moon B1F", "Route 4", one_way=True)
|
||||
connect(world, player, "Route 4", "Cerulean City")
|
||||
connect(world, player, "Cerulean City", "Cerulean Gym", one_way=True)
|
||||
connect(world, player, "Cerulean City", "Route 24", one_way=True)
|
||||
connect(world, player, "Route 24", "Route 25", one_way=True)
|
||||
connect(world, player, "Cerulean City", "Route 9", lambda state: state.pokemon_rb_can_cut(player))
|
||||
connect(world, player, "Route 9", "Route 10 North")
|
||||
connect(world, player, "Route 10 North", "Rock Tunnel 1F", lambda state: state.pokemon_rb_can_flash(player))
|
||||
connect(world, player, "Route 10 North", "Power Plant", lambda state: state.pokemon_rb_can_surf(player) and
|
||||
(state.has("Plant Key", player) or not state.world.extra_key_items[player].value), one_way=True)
|
||||
connect(world, player, "Rock Tunnel 1F", "Route 10 South", lambda state: state.pokemon_rb_can_flash(player))
|
||||
connect(world, player, "Rock Tunnel 1F", "Rock Tunnel B1F")
|
||||
connect(world, player, "Lavender Town", "Pokemon Tower 1F", one_way=True)
|
||||
connect(world, player, "Lavender Town", "Pokemon Tower 1F", one_way=True)
|
||||
connect(world, player, "Pokemon Tower 1F", "Pokemon Tower 2F", one_way=True)
|
||||
connect(world, player, "Pokemon Tower 2F", "Pokemon Tower 3F", one_way=True)
|
||||
connect(world, player, "Pokemon Tower 3F", "Pokemon Tower 4F", one_way=True)
|
||||
connect(world, player, "Pokemon Tower 4F", "Pokemon Tower 5F", one_way=True)
|
||||
connect(world, player, "Pokemon Tower 5F", "Pokemon Tower 6F", one_way=True)
|
||||
connect(world, player, "Pokemon Tower 6F", "Pokemon Tower 7F", lambda state: state.has("Silph Scope", player))
|
||||
connect(world, player, "Cerulean City", "Route 5")
|
||||
connect(world, player, "Route 5", "Saffron City", lambda state: state.pokemon_rb_can_pass_guards(player))
|
||||
connect(world, player, "Route 5", "Underground Tunnel North-South")
|
||||
connect(world, player, "Route 6", "Underground Tunnel North-South")
|
||||
connect(world, player, "Route 6", "Saffron City", lambda state: state.pokemon_rb_can_pass_guards(player))
|
||||
connect(world, player, "Route 7", "Saffron City", lambda state: state.pokemon_rb_can_pass_guards(player))
|
||||
connect(world, player, "Route 8", "Saffron City", lambda state: state.pokemon_rb_can_pass_guards(player))
|
||||
connect(world, player, "Saffron City", "Copycat's House", lambda state: state.has("Silph Co Liberated", player), one_way=True)
|
||||
connect(world, player, "Saffron City", "Saffron Gym", lambda state: state.has("Silph Co Liberated", player), one_way=True)
|
||||
connect(world, player, "Route 6", "Vermilion City")
|
||||
connect(world, player, "Vermilion City", "Vermilion Gym", lambda state: state.pokemon_rb_can_surf(player) or state.pokemon_rb_can_cut(player), one_way=True)
|
||||
connect(world, player, "Vermilion City", "S.S. Anne 1F", lambda state: state.has("S.S. Ticket", player), one_way=True)
|
||||
connect(world, player, "S.S. Anne 1F", "S.S. Anne 2F", one_way=True)
|
||||
connect(world, player, "S.S. Anne 1F", "S.S. Anne B1F", one_way=True)
|
||||
connect(world, player, "Vermilion City", "Route 11")
|
||||
connect(world, player, "Vermilion City", "Diglett's Cave")
|
||||
connect(world, player, "Route 12 West", "Route 11 East", lambda state: state.pokemon_rb_can_strength(player) or not state.world.extra_strength_boulders[player].value)
|
||||
connect(world, player, "Route 12 North", "Route 12 South", lambda state: state.has("Poke Flute", player) or state.pokemon_rb_can_surf( player))
|
||||
connect(world, player, "Route 12 West", "Route 12 North", lambda state: state.has("Poke Flute", player))
|
||||
connect(world, player, "Route 12 West", "Route 12 South", lambda state: state.has("Poke Flute", player))
|
||||
connect(world, player, "Route 12 South", "Route 12 Grass", lambda state: state.pokemon_rb_can_cut(player))
|
||||
connect(world, player, "Route 12 North", "Lavender Town")
|
||||
connect(world, player, "Route 7", "Lavender Town")
|
||||
connect(world, player, "Route 10 South", "Lavender Town")
|
||||
connect(world, player, "Route 7", "Underground Tunnel West-East")
|
||||
connect(world, player, "Route 8", "Underground Tunnel West-East")
|
||||
connect(world, player, "Route 8", "Celadon City")
|
||||
connect(world, player, "Route 8", "Route 8 Grass", lambda state: state.pokemon_rb_can_cut(player), one_way=True)
|
||||
connect(world, player, "Route 7", "Celadon City")
|
||||
connect(world, player, "Celadon City", "Celadon Gym", lambda state: state.pokemon_rb_can_cut(player), one_way=True)
|
||||
connect(world, player, "Celadon City", "Celadon Prize Corner")
|
||||
connect(world, player, "Celadon City", "Route 16")
|
||||
connect(world, player, "Route 16", "Route 16 North", lambda state: state.pokemon_rb_can_cut(player), one_way=True)
|
||||
connect(world, player, "Route 16", "Route 17", lambda state: state.has("Poke Flute", player) and state.has("Bicycle", player))
|
||||
connect(world, player, "Route 17", "Route 18", lambda state: state.has("Bicycle", player))
|
||||
connect(world, player, "Fuchsia City", "Fuchsia Gym", one_way=True)
|
||||
connect(world, player, "Fuchsia City", "Route 18")
|
||||
connect(world, player, "Fuchsia City", "Safari Zone Gate", one_way=True)
|
||||
connect(world, player, "Safari Zone Gate", "Safari Zone Center", lambda state: state.has("Safari Pass", player) or not state.world.extra_key_items[player].value, one_way=True)
|
||||
connect(world, player, "Safari Zone Center", "Safari Zone East", one_way=True)
|
||||
connect(world, player, "Safari Zone Center", "Safari Zone West", one_way=True)
|
||||
connect(world, player, "Safari Zone Center", "Safari Zone North", one_way=True)
|
||||
connect(world, player, "Fuchsia City", "Route 15")
|
||||
connect(world, player, "Route 15", "Route 14")
|
||||
connect(world, player, "Route 14", "Route 13")
|
||||
connect(world, player, "Route 13", "Route 12 South", lambda state: state.pokemon_rb_can_strength(player) or state.pokemon_rb_can_surf(player) or not state.world.extra_strength_boulders[player].value)
|
||||
connect(world, player, "Fuchsia City", "Route 19", lambda state: state.pokemon_rb_can_surf(player))
|
||||
connect(world, player, "Route 20 East", "Route 19")
|
||||
connect(world, player, "Route 20 West", "Cinnabar Island", lambda state: state.pokemon_rb_can_surf(player))
|
||||
connect(world, player, "Route 20 West", "Seafoam Islands 1F")
|
||||
connect(world, player, "Route 20 East", "Seafoam Islands 1F", one_way=True)
|
||||
connect(world, player, "Seafoam Islands 1F", "Route 20 East", lambda state: state.pokemon_rb_can_strength(player), one_way=True)
|
||||
connect(world, player, "Viridian City", "Viridian City North", lambda state: state.has("Oak's Parcel", player) or state.world.old_man[player].value == 2 or state.pokemon_rb_can_cut(player))
|
||||
connect(world, player, "Route 3", "Mt Moon 1F", one_way=True)
|
||||
connect(world, player, "Route 11", "Route 11 East", lambda state: state.pokemon_rb_can_strength(player))
|
||||
connect(world, player, "Cinnabar Island", "Cinnabar Gym", lambda state: state.has("Secret Key", player), one_way=True)
|
||||
connect(world, player, "Cinnabar Island", "Pokemon Mansion 1F", lambda state: state.has("Mansion Key", player) or not state.world.extra_key_items[player].value, one_way=True)
|
||||
connect(world, player, "Seafoam Islands 1F", "Seafoam Islands B1F", one_way=True)
|
||||
connect(world, player, "Seafoam Islands B1F", "Seafoam Islands B2F", one_way=True)
|
||||
connect(world, player, "Seafoam Islands B2F", "Seafoam Islands B3F", one_way=True)
|
||||
connect(world, player, "Seafoam Islands B3F", "Seafoam Islands B4F", one_way=True)
|
||||
connect(world, player, "Route 21", "Cinnabar Island", lambda state: state.pokemon_rb_can_surf(player))
|
||||
connect(world, player, "Pallet Town", "Route 21", lambda state: state.pokemon_rb_can_surf(player))
|
||||
connect(world, player, "Saffron City", "Silph Co 1F", lambda state: state.has("Fuji Saved", player), one_way=True)
|
||||
connect(world, player, "Silph Co 1F", "Silph Co 2F", one_way=True)
|
||||
connect(world, player, "Silph Co 2F", "Silph Co 3F", one_way=True)
|
||||
connect(world, player, "Silph Co 3F", "Silph Co 4F", one_way=True)
|
||||
connect(world, player, "Silph Co 4F", "Silph Co 5F", one_way=True)
|
||||
connect(world, player, "Silph Co 5F", "Silph Co 6F", one_way=True)
|
||||
connect(world, player, "Silph Co 6F", "Silph Co 7F", one_way=True)
|
||||
connect(world, player, "Silph Co 7F", "Silph Co 8F", one_way=True)
|
||||
connect(world, player, "Silph Co 8F", "Silph Co 9F", one_way=True)
|
||||
connect(world, player, "Silph Co 9F", "Silph Co 10F", one_way=True)
|
||||
connect(world, player, "Silph Co 10F", "Silph Co 11F", one_way=True)
|
||||
connect(world, player, "Celadon City", "Rocket Hideout B1F", lambda state: state.has("Hideout Key", player) or not state.world.extra_key_items[player].value, one_way=True)
|
||||
connect(world, player, "Rocket Hideout B1F", "Rocket Hideout B2F", one_way=True)
|
||||
connect(world, player, "Rocket Hideout B2F", "Rocket Hideout B3F", one_way=True)
|
||||
connect(world, player, "Rocket Hideout B3F", "Rocket Hideout B4F", one_way=True)
|
||||
connect(world, player, "Pokemon Mansion 1F", "Pokemon Mansion 2F", one_way=True)
|
||||
connect(world, player, "Pokemon Mansion 2F", "Pokemon Mansion 3F", one_way=True)
|
||||
connect(world, player, "Pokemon Mansion 1F", "Pokemon Mansion B1F", one_way=True)
|
||||
connect(world, player, "Route 23 North", "Victory Road 1F", lambda state: state.pokemon_rb_can_strength(player), one_way=True)
|
||||
connect(world, player, "Victory Road 1F", "Victory Road 2F", one_way=True)
|
||||
connect(world, player, "Victory Road 2F", "Victory Road 3F", one_way=True)
|
||||
connect(world, player, "Victory Road 2F", "Indigo Plateau", lambda state: state.pokemon_rb_has_badges(state.world.elite_four_condition[player], player), one_way=True)
|
||||
connect(world, player, "Cerulean City", "Cerulean Cave 1F", lambda state:
|
||||
state.pokemon_rb_cerulean_cave(state.world.cerulean_cave_condition[player].value + (state.world.extra_key_items[player].value * 4), player) and
|
||||
state.pokemon_rb_can_surf(player), one_way=True)
|
||||
connect(world, player, "Cerulean Cave 1F", "Cerulean Cave 2F", one_way=True)
|
||||
connect(world, player, "Cerulean Cave 1F", "Cerulean Cave B1F", lambda state: state.pokemon_rb_can_surf(player), one_way=True)
|
||||
if world.worlds[player].fly_map != "Pallet Town":
|
||||
connect(world, player, "Menu", world.worlds[player].fly_map, lambda state: state.pokemon_rb_can_fly(player), one_way=True,
|
||||
name="Fly to " + world.worlds[player].fly_map)
|
||||
|
||||
|
||||
def connect(world: MultiWorld, player: int, source: str, target: str, rule: callable = lambda state: True, one_way=False, name=None):
|
||||
source_region = world.get_region(source, player)
|
||||
target_region = world.get_region(target, player)
|
||||
|
||||
if name is None:
|
||||
name = source + " to " + target
|
||||
|
||||
connection = Entrance(
|
||||
player,
|
||||
name,
|
||||
source_region
|
||||
)
|
||||
|
||||
connection.access_rule = rule
|
||||
|
||||
source_region.exits.append(connection)
|
||||
connection.connect(target_region)
|
||||
if not one_way:
|
||||
connect(world, player, target, source, rule, True)
|
||||
@@ -0,0 +1,631 @@
|
||||
import os
|
||||
import hashlib
|
||||
import Utils
|
||||
import bsdiff4
|
||||
from copy import deepcopy
|
||||
from Patch import APDeltaPatch
|
||||
from .text import encode_text
|
||||
from .rom_addresses import rom_addresses
|
||||
from .locations import location_data
|
||||
import worlds.pokemon_rb.poke_data as poke_data
|
||||
|
||||
|
||||
def choose_forced_type(chances, random):
|
||||
n = random.randint(1, 100)
|
||||
for chance in chances:
|
||||
if chance[0] >= n:
|
||||
return chance[1]
|
||||
return None
|
||||
|
||||
|
||||
def filter_moves(moves, type, random):
|
||||
ret = []
|
||||
for move in moves:
|
||||
if poke_data.moves[move]["type"] == type or type is None:
|
||||
ret.append(move)
|
||||
random.shuffle(ret)
|
||||
return ret
|
||||
|
||||
|
||||
def get_move(moves, chances, random, starting_move=False):
|
||||
type = choose_forced_type(chances, random)
|
||||
filtered_moves = filter_moves(moves, type, random)
|
||||
for move in filtered_moves:
|
||||
if poke_data.moves[move]["accuracy"] > 80 and poke_data.moves[move]["power"] > 0 or not starting_move:
|
||||
moves.remove(move)
|
||||
return move
|
||||
else:
|
||||
return get_move(moves, [], random, starting_move)
|
||||
|
||||
|
||||
def get_encounter_slots(self):
|
||||
encounter_slots = [location for location in location_data if location.type == "Wild Encounter"]
|
||||
|
||||
for location in encounter_slots:
|
||||
if isinstance(location.original_item, list):
|
||||
location.original_item = location.original_item[not self.world.game_version[self.player].value]
|
||||
return encounter_slots
|
||||
|
||||
|
||||
def get_base_stat_total(mon):
|
||||
return (poke_data.pokemon_data[mon]["atk"] + poke_data.pokemon_data[mon]["def"]
|
||||
+ poke_data.pokemon_data[mon]["hp"] + poke_data.pokemon_data[mon]["spd"]
|
||||
+ poke_data.pokemon_data[mon]["spc"])
|
||||
|
||||
|
||||
def randomize_pokemon(self, mon, mons_list, randomize_type):
|
||||
if randomize_type in [1, 3]:
|
||||
type_mons = [pokemon for pokemon in mons_list if any([poke_data.pokemon_data[mon][
|
||||
"type1"] in [self.local_poke_data[pokemon]["type1"], self.local_poke_data[pokemon]["type2"]],
|
||||
poke_data.pokemon_data[mon]["type2"] in [self.local_poke_data[pokemon]["type1"],
|
||||
self.local_poke_data[pokemon]["type2"]]])]
|
||||
if not type_mons:
|
||||
type_mons = mons_list.copy()
|
||||
if randomize_type == 3:
|
||||
stat_base = get_base_stat_total(mon)
|
||||
type_mons.sort(key=lambda mon: abs(get_base_stat_total(mon) - stat_base))
|
||||
mon = type_mons[round(self.world.random.triangular(0, len(type_mons) - 1, 0))]
|
||||
if randomize_type == 2:
|
||||
stat_base = get_base_stat_total(mon)
|
||||
mons_list.sort(key=lambda mon: abs(get_base_stat_total(mon) - stat_base))
|
||||
mon = mons_list[round(self.world.random.triangular(0, 50, 0))]
|
||||
elif randomize_type == 4:
|
||||
mon = self.world.random.choice(mons_list)
|
||||
return mon
|
||||
|
||||
|
||||
def process_trainer_data(self, data):
|
||||
mons_list = [pokemon for pokemon in poke_data.pokemon_data.keys() if pokemon not in poke_data.legendary_pokemon
|
||||
or self.world.trainer_legendaries[self.player].value]
|
||||
address = rom_addresses["Trainer_Data"]
|
||||
while address < rom_addresses["Trainer_Data_End"]:
|
||||
if data[address] == 255:
|
||||
mode = 1
|
||||
else:
|
||||
mode = 0
|
||||
while True:
|
||||
address += 1
|
||||
if data[address] == 0:
|
||||
address += 1
|
||||
break
|
||||
address += mode
|
||||
mon = None
|
||||
for i in range(1, 4):
|
||||
for l in ["A", "B", "C", "D", "E", "F", "G", "H"]:
|
||||
if rom_addresses[f"Rival_Starter{i}_{l}"] == address:
|
||||
mon = " ".join(self.world.get_location(f"Pallet Town - Starter {i}", self.player).item.name.split()[1:])
|
||||
if l in ["D", "E", "F", "G", "H"] and mon in poke_data.evolves_to:
|
||||
mon = poke_data.evolves_to[mon]
|
||||
if l in ["F", "G", "H"] and mon in poke_data.evolves_to:
|
||||
mon = poke_data.evolves_to[mon]
|
||||
if mon is None and self.world.randomize_trainer_parties[self.player].value:
|
||||
mon = poke_data.id_to_mon[data[address]]
|
||||
mon = randomize_pokemon(self, mon, mons_list, self.world.randomize_trainer_parties[self.player].value)
|
||||
if mon is not None:
|
||||
data[address] = poke_data.pokemon_data[mon]["id"]
|
||||
|
||||
|
||||
def process_static_pokemon(self):
|
||||
starter_slots = [location for location in location_data if location.type == "Starter Pokemon"]
|
||||
legendary_slots = [location for location in location_data if location.type == "Legendary Pokemon"]
|
||||
static_slots = [location for location in location_data if location.type in
|
||||
["Static Pokemon", "Missable Pokemon"]]
|
||||
legendary_mons = [slot.original_item for slot in legendary_slots]
|
||||
|
||||
tower_6F_mons = set()
|
||||
for i in range(1, 11):
|
||||
tower_6F_mons.add(self.world.get_location(f"Pokemon Tower 6F - Wild Pokemon - {i}", self.player).item.name)
|
||||
|
||||
mons_list = [pokemon for pokemon in poke_data.first_stage_pokemon if pokemon not in poke_data.legendary_pokemon
|
||||
or self.world.randomize_legendary_pokemon[self.player].value == 3]
|
||||
if self.world.randomize_legendary_pokemon[self.player].value == 0:
|
||||
for slot in legendary_slots:
|
||||
location = self.world.get_location(slot.name, self.player)
|
||||
location.place_locked_item(self.create_item("Missable " + slot.original_item))
|
||||
elif self.world.randomize_legendary_pokemon[self.player].value == 1:
|
||||
self.world.random.shuffle(legendary_mons)
|
||||
for slot in legendary_slots:
|
||||
location = self.world.get_location(slot.name, self.player)
|
||||
location.place_locked_item(self.create_item("Missable " + legendary_mons.pop()))
|
||||
elif self.world.randomize_legendary_pokemon[self.player].value == 2:
|
||||
static_slots = static_slots + legendary_slots
|
||||
self.world.random.shuffle(static_slots)
|
||||
static_slots.sort(key=lambda s: 0 if s.name == "Pokemon Tower 6F - Restless Soul" else 1)
|
||||
while legendary_slots:
|
||||
swap_slot = legendary_slots.pop()
|
||||
slot = static_slots.pop()
|
||||
slot_type = slot.type.split()[0]
|
||||
if slot_type == "Legendary":
|
||||
slot_type = "Missable"
|
||||
location = self.world.get_location(slot.name, self.player)
|
||||
location.place_locked_item(self.create_item(slot_type + " " + swap_slot.original_item))
|
||||
swap_slot.original_item = slot.original_item
|
||||
elif self.world.randomize_legendary_pokemon[self.player].value == 3:
|
||||
static_slots = static_slots + legendary_slots
|
||||
|
||||
for slot in static_slots:
|
||||
location = self.world.get_location(slot.name, self.player)
|
||||
randomize_type = self.world.randomize_static_pokemon[self.player].value
|
||||
slot_type = slot.type.split()[0]
|
||||
if slot_type == "Legendary":
|
||||
slot_type = "Missable"
|
||||
if not randomize_type:
|
||||
location.place_locked_item(self.create_item(slot_type + " " + slot.original_item))
|
||||
else:
|
||||
mon = self.create_item(slot_type + " " +
|
||||
randomize_pokemon(self, slot.original_item, mons_list, randomize_type))
|
||||
while location.name == "Pokemon Tower 6F - Restless Soul" and mon in tower_6F_mons:
|
||||
mon = self.create_item(slot_type + " " + randomize_pokemon(self, slot.original_item, mons_list,
|
||||
randomize_type))
|
||||
location.place_locked_item(mon)
|
||||
|
||||
for slot in starter_slots:
|
||||
location = self.world.get_location(slot.name, self.player)
|
||||
randomize_type = self.world.randomize_starter_pokemon[self.player].value
|
||||
slot_type = "Missable"
|
||||
if not randomize_type:
|
||||
location.place_locked_item(self.create_item(slot_type + " " + slot.original_item))
|
||||
else:
|
||||
location.place_locked_item(self.create_item(slot_type + " " +
|
||||
randomize_pokemon(self, slot.original_item, mons_list, randomize_type)))
|
||||
|
||||
|
||||
def process_wild_pokemon(self):
|
||||
|
||||
encounter_slots = get_encounter_slots(self)
|
||||
|
||||
placed_mons = {pokemon: 0 for pokemon in poke_data.pokemon_data.keys()}
|
||||
if self.world.randomize_wild_pokemon[self.player].value:
|
||||
mons_list = [pokemon for pokemon in poke_data.pokemon_data.keys() if pokemon not in poke_data.legendary_pokemon
|
||||
or self.world.randomize_legendary_pokemon[self.player].value == 3]
|
||||
self.world.random.shuffle(encounter_slots)
|
||||
locations = []
|
||||
for slot in encounter_slots:
|
||||
mon = randomize_pokemon(self, slot.original_item, mons_list, self.world.randomize_wild_pokemon[self.player].value)
|
||||
# if static Pokemon are not randomized, we make sure nothing on Pokemon Tower 6F is a Marowak
|
||||
# if static Pokemon are randomized we deal with that during static encounter randomization
|
||||
while (self.world.randomize_static_pokemon[self.player].value == 0 and mon == "Marowak"
|
||||
and "Pokemon Tower 6F" in slot.name):
|
||||
# to account for the possibility that only one ground type Pokemon exists, match only stats for this fix
|
||||
mon = randomize_pokemon(self, slot.original_item, mons_list, 2)
|
||||
placed_mons[mon] += 1
|
||||
location = self.world.get_location(slot.name, self.player)
|
||||
location.item = self.create_item(mon)
|
||||
location.event = True
|
||||
location.locked = True
|
||||
location.item.location = location
|
||||
locations.append(location)
|
||||
|
||||
mons_to_add = []
|
||||
remaining_pokemon = [pokemon for pokemon in poke_data.pokemon_data.keys() if placed_mons[pokemon] == 0 and
|
||||
(pokemon not in poke_data.legendary_pokemon or self.world.randomize_legendary_pokemon[self.player].value == 3)]
|
||||
if self.world.catch_em_all[self.player].value == 1:
|
||||
mons_to_add = [pokemon for pokemon in poke_data.first_stage_pokemon if placed_mons[pokemon] == 0 and
|
||||
(pokemon not in poke_data.legendary_pokemon or self.world.randomize_legendary_pokemon[self.player].value == 3)]
|
||||
elif self.world.catch_em_all[self.player].value == 2:
|
||||
mons_to_add = remaining_pokemon.copy()
|
||||
logic_needed_mons = max(self.world.oaks_aide_rt_2[self.player].value,
|
||||
self.world.oaks_aide_rt_11[self.player].value,
|
||||
self.world.oaks_aide_rt_15[self.player].value)
|
||||
if self.world.accessibility[self.player] == "minimal":
|
||||
logic_needed_mons = 0
|
||||
|
||||
self.world.random.shuffle(remaining_pokemon)
|
||||
while (len([pokemon for pokemon in placed_mons if placed_mons[pokemon] > 0])
|
||||
+ len(mons_to_add) < logic_needed_mons):
|
||||
mons_to_add.append(remaining_pokemon.pop())
|
||||
for mon in mons_to_add:
|
||||
stat_base = get_base_stat_total(mon)
|
||||
candidate_locations = get_encounter_slots(self)
|
||||
if self.world.randomize_wild_pokemon[self.player].value in [1, 3]:
|
||||
candidate_locations = [slot for slot in candidate_locations if any([poke_data.pokemon_data[slot.original_item][
|
||||
"type1"] in [self.local_poke_data[mon]["type1"], self.local_poke_data[mon]["type2"]],
|
||||
poke_data.pokemon_data[slot.original_item]["type2"] in [self.local_poke_data[mon]["type1"],
|
||||
self.local_poke_data[mon]["type2"]]])]
|
||||
if not candidate_locations:
|
||||
candidate_locations = location_data
|
||||
candidate_locations = [self.world.get_location(location.name, self.player) for location in candidate_locations]
|
||||
candidate_locations.sort(key=lambda slot: abs(get_base_stat_total(slot.item.name) - stat_base))
|
||||
for location in candidate_locations:
|
||||
if placed_mons[location.item.name] > 1 or location.item.name not in poke_data.first_stage_pokemon:
|
||||
placed_mons[location.item.name] -= 1
|
||||
location.item = self.create_item(mon)
|
||||
location.item.location = location
|
||||
placed_mons[mon] += 1
|
||||
break
|
||||
|
||||
else:
|
||||
for slot in encounter_slots:
|
||||
location = self.world.get_location(slot.name, self.player)
|
||||
location.item = self.create_item(slot.original_item)
|
||||
location.event = True
|
||||
location.locked = True
|
||||
location.item.location = location
|
||||
placed_mons[location.item.name] += 1
|
||||
|
||||
|
||||
def process_pokemon_data(self):
|
||||
|
||||
local_poke_data = deepcopy(poke_data.pokemon_data)
|
||||
learnsets = deepcopy(poke_data.learnsets)
|
||||
|
||||
for mon, mon_data in local_poke_data.items():
|
||||
if self.world.randomize_pokemon_stats[self.player].value == 1:
|
||||
stats = [mon_data["hp"], mon_data["atk"], mon_data["def"], mon_data["spd"], mon_data["spc"]]
|
||||
self.world.random.shuffle(stats)
|
||||
mon_data["hp"] = stats[0]
|
||||
mon_data["atk"] = stats[1]
|
||||
mon_data["def"] = stats[2]
|
||||
mon_data["spd"] = stats[3]
|
||||
mon_data["spc"] = stats[4]
|
||||
elif self.world.randomize_pokemon_stats[self.player].value == 2:
|
||||
old_stats = mon_data["hp"] + mon_data["atk"] + mon_data["def"] + mon_data["spd"] + mon_data["spc"] - 5
|
||||
stats = [1, 1, 1, 1, 1]
|
||||
while old_stats > 0:
|
||||
stat = self.world.random.randint(0, 4)
|
||||
if stats[stat] < 255:
|
||||
old_stats -= 1
|
||||
stats[stat] += 1
|
||||
mon_data["hp"] = stats[0]
|
||||
mon_data["atk"] = stats[1]
|
||||
mon_data["def"] = stats[2]
|
||||
mon_data["spd"] = stats[3]
|
||||
mon_data["spc"] = stats[4]
|
||||
if self.world.randomize_pokemon_types[self.player].value:
|
||||
if self.world.randomize_pokemon_types[self.player].value == 1 and mon in poke_data.evolves_from:
|
||||
type1 = local_poke_data[poke_data.evolves_from[mon]]["type1"]
|
||||
type2 = local_poke_data[poke_data.evolves_from[mon]]["type2"]
|
||||
if type1 == type2:
|
||||
if self.world.secondary_type_chance[self.player].value == -1:
|
||||
if mon_data["type1"] != mon_data["type2"]:
|
||||
while type2 == type1:
|
||||
type2 = self.world.random.choice(list(poke_data.type_names.values()))
|
||||
elif self.world.random.randint(1, 100) <= self.world.secondary_type_chance[self.player].value:
|
||||
type2 = self.world.random.choice(list(poke_data.type_names.values()))
|
||||
else:
|
||||
type1 = self.world.random.choice(list(poke_data.type_names.values()))
|
||||
type2 = type1
|
||||
if ((self.world.secondary_type_chance[self.player].value == -1 and mon_data["type1"]
|
||||
!= mon_data["type2"]) or self.world.random.randint(1, 100)
|
||||
<= self.world.secondary_type_chance[self.player].value):
|
||||
while type2 == type1:
|
||||
type2 = self.world.random.choice(list(poke_data.type_names.values()))
|
||||
|
||||
mon_data["type1"] = type1
|
||||
mon_data["type2"] = type2
|
||||
if self.world.randomize_pokemon_movesets[self.player].value:
|
||||
if self.world.randomize_pokemon_movesets[self.player].value == 1:
|
||||
if mon_data["type1"] == "Normal" and mon_data["type2"] == "Normal":
|
||||
chances = [[75, "Normal"]]
|
||||
elif mon_data["type1"] == "Normal" or mon_data["type2"] == "Normal":
|
||||
if mon_data["type1"] == "Normal":
|
||||
second_type = mon_data["type2"]
|
||||
else:
|
||||
second_type = mon_data["type1"]
|
||||
chances = [[30, "Normal"], [85, second_type]]
|
||||
elif mon_data["type1"] == mon_data["type2"]:
|
||||
chances = [[60, mon_data["type1"]], [80, "Normal"]]
|
||||
else:
|
||||
chances = [[50, mon_data["type1"]], [80, mon_data["type2"]], [85, "Normal"]]
|
||||
else:
|
||||
chances = []
|
||||
moves = list(poke_data.moves.keys())
|
||||
for move in ["No Move"] + poke_data.hm_moves:
|
||||
moves.remove(move)
|
||||
mon_data["start move 1"] = get_move(moves, chances, self.world.random, True)
|
||||
for i in range(2, 5):
|
||||
if mon_data[f"start move {i}"] != "No Move" or self.world.start_with_four_moves[
|
||||
self.player].value == 1:
|
||||
mon_data[f"start move {i}"] = get_move(moves, chances, self.world.random)
|
||||
if mon in learnsets:
|
||||
for move_num in range(0, len(learnsets[mon])):
|
||||
learnsets[mon][move_num] = get_move(moves, chances, self.world.random)
|
||||
if self.world.randomize_pokemon_catch_rates[self.player].value:
|
||||
mon_data["catch rate"] = self.world.random.randint(self.world.minimum_catch_rate[self.player], 255)
|
||||
else:
|
||||
mon_data["catch rate"] = max(self.world.minimum_catch_rate[self.player], mon_data["catch rate"])
|
||||
|
||||
if mon in poke_data.evolves_from.keys() and mon_data["type1"] == local_poke_data[poke_data.evolves_from[mon]]["type1"] and mon_data["type2"] == local_poke_data[poke_data.evolves_from[mon]]["type2"]:
|
||||
mon_data["tms"] = local_poke_data[poke_data.evolves_from[mon]]["tms"]
|
||||
elif mon != "Mew":
|
||||
tms_hms = poke_data.tm_moves + poke_data.hm_moves
|
||||
for flag, tm_move in enumerate(tms_hms):
|
||||
if (flag < 50 and self.world.tm_compatibility[self.player].value == 1) or (flag >= 50 and self.world.hm_compatibility[self.player].value == 1):
|
||||
type_match = poke_data.moves[tm_move]["type"] in [mon_data["type1"], mon_data["type2"]]
|
||||
bit = int(self.world.random.randint(1, 100) < [[90, 50, 25], [100, 75, 25]][flag >= 50][0 if type_match else 1 if poke_data.moves[tm_move]["type"] == "Normal" else 2])
|
||||
elif (flag < 50 and self.world.tm_compatibility[self.player].value == 2) or (flag >= 50 and self.world.hm_compatibility[self.player].value == 2):
|
||||
bit = [0, 1][self.world.random.randint(0, 1)]
|
||||
elif (flag < 50 and self.world.tm_compatibility[self.player].value == 3) or (flag >= 50 and self.world.hm_compatibility[self.player].value == 3):
|
||||
bit = 1
|
||||
else:
|
||||
continue
|
||||
if bit:
|
||||
mon_data["tms"][int(flag / 8)] |= 1 << (flag % 8)
|
||||
else:
|
||||
mon_data["tms"][int(flag / 8)] &= ~(1 << (flag % 8))
|
||||
|
||||
self.local_poke_data = local_poke_data
|
||||
self.learnsets = learnsets
|
||||
|
||||
|
||||
|
||||
def generate_output(self, output_directory: str):
|
||||
random = self.world.slot_seeds[self.player]
|
||||
game_version = self.world.game_version[self.player].current_key
|
||||
data = bytearray(get_base_rom_bytes(game_version))
|
||||
|
||||
basemd5 = hashlib.md5()
|
||||
basemd5.update(data)
|
||||
|
||||
for location in self.world.get_locations():
|
||||
if location.player != self.player or location.rom_address is None:
|
||||
continue
|
||||
if location.item and location.item.player == self.player:
|
||||
if location.rom_address:
|
||||
rom_address = location.rom_address
|
||||
if not isinstance(rom_address, list):
|
||||
rom_address = [rom_address]
|
||||
for address in rom_address:
|
||||
if location.item.name in poke_data.pokemon_data.keys():
|
||||
data[address] = poke_data.pokemon_data[location.item.name]["id"]
|
||||
elif " ".join(location.item.name.split()[1:]) in poke_data.pokemon_data.keys():
|
||||
data[address] = poke_data.pokemon_data[" ".join(location.item.name.split()[1:])]["id"]
|
||||
else:
|
||||
data[address] = self.item_name_to_id[location.item.name] - 172000000
|
||||
|
||||
else:
|
||||
data[location.rom_address] = 0x2C # AP Item
|
||||
data[rom_addresses['Fly_Location']] = self.fly_map_code
|
||||
|
||||
if self.world.tea[self.player].value:
|
||||
data[rom_addresses["Option_Tea"]] = 1
|
||||
data[rom_addresses["Guard_Drink_List"]] = 0x54
|
||||
data[rom_addresses["Guard_Drink_List"] + 1] = 0
|
||||
data[rom_addresses["Guard_Drink_List"] + 2] = 0
|
||||
|
||||
if self.world.extra_key_items[self.player].value:
|
||||
data[rom_addresses['Options']] |= 4
|
||||
data[rom_addresses["Option_Blind_Trainers"]] = round(self.world.blind_trainers[self.player].value * 2.55)
|
||||
data[rom_addresses['Option_Cerulean_Cave_Condition']] = self.world.cerulean_cave_condition[self.player].value
|
||||
data[rom_addresses['Option_Encounter_Minimum_Steps']] = self.world.minimum_steps_between_encounters[self.player].value
|
||||
data[rom_addresses['Option_Victory_Road_Badges']] = self.world.victory_road_condition[self.player].value
|
||||
data[rom_addresses['Option_Pokemon_League_Badges']] = self.world.elite_four_condition[self.player].value
|
||||
data[rom_addresses['Option_Viridian_Gym_Badges']] = self.world.viridian_gym_condition[self.player].value
|
||||
data[rom_addresses['Option_EXP_Modifier']] = self.world.exp_modifier[self.player].value
|
||||
if not self.world.require_item_finder[self.player].value:
|
||||
data[rom_addresses['Option_Itemfinder']] = 0
|
||||
if self.world.extra_strength_boulders[self.player].value:
|
||||
for i in range(0, 3):
|
||||
data[rom_addresses['Option_Boulders'] + (i * 3)] = 0x15
|
||||
if self.world.extra_key_items[self.player].value:
|
||||
for i in range(0, 4):
|
||||
data[rom_addresses['Option_Rock_Tunnel_Extra_Items'] + (i * 3)] = 0x15
|
||||
if self.world.old_man[self.player].value == 2:
|
||||
data[rom_addresses['Option_Old_Man']] = 0x11
|
||||
data[rom_addresses['Option_Old_Man_Lying']] = 0x15
|
||||
money = str(self.world.starting_money[self.player].value)
|
||||
while len(money) < 6:
|
||||
money = "0" + money
|
||||
data[rom_addresses["Starting_Money_High"]] = int(money[:2], 16)
|
||||
data[rom_addresses["Starting_Money_Middle"]] = int(money[2:4], 16)
|
||||
data[rom_addresses["Starting_Money_Low"]] = int(money[4:], 16)
|
||||
data[rom_addresses["Text_Badges_Needed"]] = encode_text(
|
||||
str(max(self.world.victory_road_condition[self.player].value,
|
||||
self.world.elite_four_condition[self.player].value)))[0]
|
||||
if self.world.badges_needed_for_hm_moves[self.player].value == 0:
|
||||
for hm_move in poke_data.hm_moves:
|
||||
write_bytes(data, bytearray([0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00, 0x00]),
|
||||
rom_addresses["HM_" + hm_move + "_Badge_a"])
|
||||
elif self.extra_badges:
|
||||
written_badges = {}
|
||||
for hm_move, badge in self.extra_badges.items():
|
||||
data[rom_addresses["HM_" + hm_move + "_Badge_b"]] = {"Boulder Badge": 0x47, "Cascade Badge": 0x4F,
|
||||
"Thunder Badge": 0x57, "Rainbow Badge": 0x5F,
|
||||
"Soul Badge": 0x67, "Marsh Badge": 0x6F,
|
||||
"Volcano Badge": 0x77, "Earth Badge": 0x7F}[badge]
|
||||
move_text = hm_move
|
||||
if badge not in ["Marsh Badge", "Volcano Badge", "Earth Badge"]:
|
||||
move_text = ", " + move_text
|
||||
rom_address = rom_addresses["Badge_Text_" + badge.replace(" ", "_")]
|
||||
if badge in written_badges:
|
||||
rom_address += len(written_badges[badge])
|
||||
move_text = ", " + move_text
|
||||
write_bytes(data, encode_text(move_text.upper()), rom_address)
|
||||
written_badges[badge] = move_text
|
||||
for badge in ["Marsh Badge", "Volcano Badge", "Earth Badge"]:
|
||||
if badge not in written_badges:
|
||||
write_bytes(data, encode_text("Nothing"), rom_addresses["Badge_Text_" + badge.replace(" ", "_")])
|
||||
|
||||
chart = deepcopy(poke_data.type_chart)
|
||||
if self.world.randomize_type_matchup_types[self.player].value == 1:
|
||||
attacking_types = []
|
||||
defending_types = []
|
||||
for matchup in chart:
|
||||
attacking_types.append(matchup[0])
|
||||
defending_types.append(matchup[1])
|
||||
random.shuffle(attacking_types)
|
||||
random.shuffle(defending_types)
|
||||
matchups = []
|
||||
while len(attacking_types) > 0:
|
||||
if [attacking_types[0], defending_types[0]] not in matchups:
|
||||
matchups.append([attacking_types.pop(0), defending_types.pop(0)])
|
||||
else:
|
||||
matchup = matchups.pop(0)
|
||||
attacking_types.append(matchup[0])
|
||||
defending_types.append(matchup[1])
|
||||
random.shuffle(attacking_types)
|
||||
random.shuffle(defending_types)
|
||||
for matchup, chart_row in zip(matchups, chart):
|
||||
chart_row[0] = matchup[0]
|
||||
chart_row[1] = matchup[1]
|
||||
elif self.world.randomize_type_matchup_types[self.player].value == 2:
|
||||
used_matchups = []
|
||||
for matchup in chart:
|
||||
matchup[0] = random.choice(list(poke_data.type_names.values()))
|
||||
matchup[1] = random.choice(list(poke_data.type_names.values()))
|
||||
while [matchup[0], matchup[1]] in used_matchups:
|
||||
matchup[0] = random.choice(list(poke_data.type_names.values()))
|
||||
matchup[1] = random.choice(list(poke_data.type_names.values()))
|
||||
used_matchups.append([matchup[0], matchup[1]])
|
||||
if self.world.randomize_type_matchup_type_effectiveness[self.player].value == 1:
|
||||
effectiveness_list = []
|
||||
for matchup in chart:
|
||||
effectiveness_list.append(matchup[2])
|
||||
random.shuffle(effectiveness_list)
|
||||
for (matchup, effectiveness) in zip(chart, effectiveness_list):
|
||||
matchup[2] = effectiveness
|
||||
elif self.world.randomize_type_matchup_type_effectiveness[self.player].value == 2:
|
||||
for matchup in chart:
|
||||
matchup[2] = random.choice([0] + ([5, 20] * 5))
|
||||
elif self.world.randomize_type_matchup_type_effectiveness[self.player].value == 3:
|
||||
for matchup in chart:
|
||||
matchup[2] = random.choice([i for i in range(0, 21) if i != 10])
|
||||
type_loc = rom_addresses["Type_Chart"]
|
||||
for matchup in chart:
|
||||
data[type_loc] = poke_data.type_ids[matchup[0]]
|
||||
data[type_loc + 1] = poke_data.type_ids[matchup[1]]
|
||||
data[type_loc + 2] = matchup[2]
|
||||
type_loc += 3
|
||||
# sort so that super-effective matchups occur first, to prevent dual "not very effective" / "super effective"
|
||||
# matchups from leading to damage being ultimately divided by 2 and then multiplied by 2, which can lead to
|
||||
# damage being reduced by 1 which leads to a "not very effective" message appearing due to my changes
|
||||
# to the way effectiveness messages are generated.
|
||||
self.type_chart = sorted(chart, key=lambda matchup: 0 - matchup[2])
|
||||
|
||||
if self.world.normalize_encounter_chances[self.player].value:
|
||||
chances = [25, 51, 77, 103, 129, 155, 180, 205, 230, 255]
|
||||
for i, chance in enumerate(chances):
|
||||
data[rom_addresses['Encounter_Chances'] + (i * 2)] = chance
|
||||
|
||||
for mon, mon_data in self.local_poke_data.items():
|
||||
if mon == "Mew":
|
||||
address = rom_addresses["Base_Stats_Mew"]
|
||||
else:
|
||||
address = rom_addresses["Base_Stats"] + (28 * (mon_data["dex"] - 1))
|
||||
data[address + 1] = self.local_poke_data[mon]["hp"]
|
||||
data[address + 2] = self.local_poke_data[mon]["atk"]
|
||||
data[address + 3] = self.local_poke_data[mon]["def"]
|
||||
data[address + 4] = self.local_poke_data[mon]["spd"]
|
||||
data[address + 5] = self.local_poke_data[mon]["spc"]
|
||||
data[address + 6] = poke_data.type_ids[self.local_poke_data[mon]["type1"]]
|
||||
data[address + 7] = poke_data.type_ids[self.local_poke_data[mon]["type2"]]
|
||||
data[address + 8] = self.local_poke_data[mon]["catch rate"]
|
||||
data[address + 15] = poke_data.moves[self.local_poke_data[mon]["start move 1"]]["id"]
|
||||
data[address + 16] = poke_data.moves[self.local_poke_data[mon]["start move 2"]]["id"]
|
||||
data[address + 17] = poke_data.moves[self.local_poke_data[mon]["start move 3"]]["id"]
|
||||
data[address + 18] = poke_data.moves[self.local_poke_data[mon]["start move 4"]]["id"]
|
||||
write_bytes(data, self.local_poke_data[mon]["tms"], address + 20)
|
||||
if mon in self.learnsets:
|
||||
address = rom_addresses["Learnset_" + mon.replace(" ", "")]
|
||||
for i, move in enumerate(self.learnsets[mon]):
|
||||
data[(address + 1) + i * 2] = poke_data.moves[move]["id"]
|
||||
|
||||
data[rom_addresses["Option_Aide_Rt2"]] = self.world.oaks_aide_rt_2[self.player]
|
||||
data[rom_addresses["Option_Aide_Rt11"]] = self.world.oaks_aide_rt_11[self.player]
|
||||
data[rom_addresses["Option_Aide_Rt15"]] = self.world.oaks_aide_rt_15[self.player]
|
||||
|
||||
if self.world.safari_zone_normal_battles[self.player].value == 1:
|
||||
data[rom_addresses["Option_Safari_Zone_Battle_Type"]] = 255
|
||||
|
||||
if self.world.reusable_tms[self.player].value:
|
||||
data[rom_addresses["Option_Reusable_TMs"]] = 0xC9
|
||||
|
||||
process_trainer_data(self, data)
|
||||
|
||||
mons = [mon["id"] for mon in poke_data.pokemon_data.values()]
|
||||
random.shuffle(mons)
|
||||
data[rom_addresses['Title_Mon_First']] = mons.pop()
|
||||
for mon in range(0, 16):
|
||||
data[rom_addresses['Title_Mons'] + mon] = mons.pop()
|
||||
if self.world.game_version[self.player].value:
|
||||
mons.sort(key=lambda mon: 0 if mon == self.world.get_location("Pallet Town - Starter 1", self.player).item.name
|
||||
else 1 if mon == self.world.get_location("Pallet Town - Starter 2", self.player).item.name else
|
||||
2 if mon == self.world.get_location("Pallet Town - Starter 3", self.player).item.name else 3)
|
||||
else:
|
||||
mons.sort(key=lambda mon: 0 if mon == self.world.get_location("Pallet Town - Starter 2", self.player).item.name
|
||||
else 1 if mon == self.world.get_location("Pallet Town - Starter 1", self.player).item.name else
|
||||
2 if mon == self.world.get_location("Pallet Town - Starter 3", self.player).item.name else 3)
|
||||
write_bytes(data, encode_text(self.world.seed_name, 20, True), rom_addresses['Title_Seed'])
|
||||
|
||||
slot_name = self.world.player_name[self.player]
|
||||
slot_name.replace("@", " ")
|
||||
slot_name.replace("<", " ")
|
||||
slot_name.replace(">", " ")
|
||||
write_bytes(data, encode_text(slot_name, 16, True, True), rom_addresses['Title_Slot_Name'])
|
||||
|
||||
write_bytes(data, self.trainer_name, rom_addresses['Player_Name'])
|
||||
write_bytes(data, self.rival_name, rom_addresses['Rival_Name'])
|
||||
|
||||
write_bytes(data, basemd5.digest(), 0xFFCC)
|
||||
write_bytes(data, self.world.seed_name.encode(), 0xFFDC)
|
||||
write_bytes(data, self.world.player_name[self.player].encode(), 0xFFF0)
|
||||
|
||||
|
||||
|
||||
outfilepname = f'_P{self.player}'
|
||||
outfilepname += f"_{self.world.get_file_safe_player_name(self.player).replace(' ', '_')}" \
|
||||
if self.world.player_name[self.player] != 'Player%d' % self.player else ''
|
||||
rompath = os.path.join(output_directory, f'AP_{self.world.seed_name}{outfilepname}.gb')
|
||||
with open(rompath, 'wb') as outfile:
|
||||
outfile.write(data)
|
||||
if self.world.game_version[self.player].current_key == "red":
|
||||
patch = RedDeltaPatch(os.path.splitext(rompath)[0] + RedDeltaPatch.patch_file_ending, player=self.player,
|
||||
player_name=self.world.player_name[self.player], patched_path=rompath)
|
||||
else:
|
||||
patch = BlueDeltaPatch(os.path.splitext(rompath)[0] + BlueDeltaPatch.patch_file_ending, player=self.player,
|
||||
player_name=self.world.player_name[self.player], patched_path=rompath)
|
||||
|
||||
patch.write()
|
||||
os.unlink(rompath)
|
||||
|
||||
|
||||
def write_bytes(data, byte_array, address):
|
||||
for byte in byte_array:
|
||||
data[address] = byte
|
||||
address += 1
|
||||
|
||||
|
||||
def get_base_rom_bytes(game_version: str, hash: str="") -> bytes:
|
||||
file_name = get_base_rom_path(game_version)
|
||||
with open(file_name, "rb") as file:
|
||||
base_rom_bytes = bytes(file.read())
|
||||
if hash:
|
||||
basemd5 = hashlib.md5()
|
||||
basemd5.update(base_rom_bytes)
|
||||
if hash != basemd5.hexdigest():
|
||||
raise Exception('Supplied Base Rom does not match known MD5 for US(1.0) release. '
|
||||
'Get the correct game and version, then dump it')
|
||||
with open(os.path.join(os.path.dirname(__file__), f'basepatch_{game_version}.bsdiff4'), 'rb') as stream:
|
||||
base_patch = bytes(stream.read())
|
||||
base_rom_bytes = bsdiff4.patch(base_rom_bytes, base_patch)
|
||||
return base_rom_bytes
|
||||
|
||||
|
||||
def get_base_rom_path(game_version: str) -> str:
|
||||
options = Utils.get_options()
|
||||
file_name = options["pokemon_rb_options"][f"{game_version}_rom_file"]
|
||||
if not os.path.exists(file_name):
|
||||
file_name = Utils.local_path(file_name)
|
||||
return file_name
|
||||
|
||||
|
||||
class BlueDeltaPatch(APDeltaPatch):
|
||||
patch_file_ending = ".apblue"
|
||||
hash = "50927e843568814f7ed45ec4f944bd8b"
|
||||
game_version = "blue"
|
||||
game = "Pokemon Red and Blue"
|
||||
result_file_ending = ".gb"
|
||||
@classmethod
|
||||
def get_source_data(cls) -> bytes:
|
||||
return get_base_rom_bytes(cls.game_version, cls.hash)
|
||||
|
||||
|
||||
class RedDeltaPatch(APDeltaPatch):
|
||||
patch_file_ending = ".apred"
|
||||
hash = "3d45c1ee9abd5738df46d2bdda8b57dc"
|
||||
game_version = "red"
|
||||
game = "Pokemon Red and Blue"
|
||||
result_file_ending = ".gb"
|
||||
@classmethod
|
||||
def get_source_data(cls) -> bytes:
|
||||
return get_base_rom_bytes(cls.game_version, cls.hash)
|
||||
@@ -0,0 +1,588 @@
|
||||
rom_addresses = {
|
||||
"Option_Encounter_Minimum_Steps": 0x3c3,
|
||||
"Option_Blind_Trainers": 0x317e,
|
||||
"Base_Stats_Mew": 0x425b,
|
||||
"Title_Mon_First": 0x436e,
|
||||
"Title_Mons": 0x4547,
|
||||
"Player_Name": 0x4569,
|
||||
"Rival_Name": 0x4571,
|
||||
"Title_Seed": 0x5dfe,
|
||||
"Title_Slot_Name": 0x5e1e,
|
||||
"PC_Item": 0x61ec,
|
||||
"PC_Item_Quantity": 0x61f1,
|
||||
"Options": 0x61f9,
|
||||
"Fly_Location": 0x61fe,
|
||||
"Option_Old_Man": 0xcaef,
|
||||
"Option_Old_Man_Lying": 0xcaf2,
|
||||
"Option_Boulders": 0xcd98,
|
||||
"Option_Rock_Tunnel_Extra_Items": 0xcda1,
|
||||
"Wild_Route1": 0xd0fb,
|
||||
"Wild_Route2": 0xd111,
|
||||
"Wild_Route22": 0xd127,
|
||||
"Wild_ViridianForest": 0xd13d,
|
||||
"Wild_Route3": 0xd153,
|
||||
"Wild_MtMoon1F": 0xd169,
|
||||
"Wild_MtMoonB1F": 0xd17f,
|
||||
"Wild_MtMoonB2F": 0xd195,
|
||||
"Wild_Route4": 0xd1ab,
|
||||
"Wild_Route24": 0xd1c1,
|
||||
"Wild_Route25": 0xd1d7,
|
||||
"Wild_Route9": 0xd1ed,
|
||||
"Wild_Route5": 0xd203,
|
||||
"Wild_Route6": 0xd219,
|
||||
"Wild_Route11": 0xd22f,
|
||||
"Wild_RockTunnel1F": 0xd245,
|
||||
"Wild_RockTunnelB1F": 0xd25b,
|
||||
"Wild_Route10": 0xd271,
|
||||
"Wild_Route12": 0xd287,
|
||||
"Wild_Route8": 0xd29d,
|
||||
"Wild_Route7": 0xd2b3,
|
||||
"Wild_PokemonTower3F": 0xd2cd,
|
||||
"Wild_PokemonTower4F": 0xd2e3,
|
||||
"Wild_PokemonTower5F": 0xd2f9,
|
||||
"Wild_PokemonTower6F": 0xd30f,
|
||||
"Wild_PokemonTower7F": 0xd325,
|
||||
"Wild_Route13": 0xd33b,
|
||||
"Wild_Route14": 0xd351,
|
||||
"Wild_Route15": 0xd367,
|
||||
"Wild_Route16": 0xd37d,
|
||||
"Wild_Route17": 0xd393,
|
||||
"Wild_Route18": 0xd3a9,
|
||||
"Wild_SafariZoneCenter": 0xd3bf,
|
||||
"Wild_SafariZoneEast": 0xd3d5,
|
||||
"Wild_SafariZoneNorth": 0xd3eb,
|
||||
"Wild_SafariZoneWest": 0xd401,
|
||||
"Wild_SeaRoutes": 0xd418,
|
||||
"Wild_SeafoamIslands1F": 0xd42d,
|
||||
"Wild_SeafoamIslandsB1F": 0xd443,
|
||||
"Wild_SeafoamIslandsB2F": 0xd459,
|
||||
"Wild_SeafoamIslandsB3F": 0xd46f,
|
||||
"Wild_SeafoamIslandsB4F": 0xd485,
|
||||
"Wild_PokemonMansion1F": 0xd49b,
|
||||
"Wild_PokemonMansion2F": 0xd4b1,
|
||||
"Wild_PokemonMansion3F": 0xd4c7,
|
||||
"Wild_PokemonMansionB1F": 0xd4dd,
|
||||
"Wild_Route21": 0xd4f3,
|
||||
"Wild_Surf_Route21": 0xd508,
|
||||
"Wild_CeruleanCave1F": 0xd51d,
|
||||
"Wild_CeruleanCave2F": 0xd533,
|
||||
"Wild_CeruleanCaveB1F": 0xd549,
|
||||
"Wild_PowerPlant": 0xd55f,
|
||||
"Wild_Route23": 0xd575,
|
||||
"Wild_VictoryRoad2F": 0xd58b,
|
||||
"Wild_VictoryRoad3F": 0xd5a1,
|
||||
"Wild_VictoryRoad1F": 0xd5b7,
|
||||
"Wild_DiglettsCave": 0xd5cd,
|
||||
"Ghost_Battle5": 0xd723,
|
||||
"HM_Surf_Badge_a": 0xda11,
|
||||
"HM_Surf_Badge_b": 0xda16,
|
||||
"Wild_Old_Rod": 0xe313,
|
||||
"Wild_Good_Rod": 0xe340,
|
||||
"Option_Reusable_TMs": 0xe60c,
|
||||
"Wild_Super_Rod_A": 0xea40,
|
||||
"Wild_Super_Rod_B": 0xea45,
|
||||
"Wild_Super_Rod_C": 0xea4a,
|
||||
"Wild_Super_Rod_D": 0xea51,
|
||||
"Wild_Super_Rod_E": 0xea56,
|
||||
"Wild_Super_Rod_F": 0xea5b,
|
||||
"Wild_Super_Rod_G": 0xea64,
|
||||
"Wild_Super_Rod_H": 0xea6d,
|
||||
"Wild_Super_Rod_I": 0xea76,
|
||||
"Wild_Super_Rod_J": 0xea7f,
|
||||
"Starting_Money_High": 0xf949,
|
||||
"Starting_Money_Middle": 0xf94c,
|
||||
"Starting_Money_Low": 0xf94f,
|
||||
"HM_Fly_Badge_a": 0x1318e,
|
||||
"HM_Fly_Badge_b": 0x13193,
|
||||
"HM_Cut_Badge_a": 0x131c4,
|
||||
"HM_Cut_Badge_b": 0x131c9,
|
||||
"HM_Strength_Badge_a": 0x131f4,
|
||||
"HM_Strength_Badge_b": 0x131f9,
|
||||
"HM_Flash_Badge_a": 0x13208,
|
||||
"HM_Flash_Badge_b": 0x1320d,
|
||||
"Encounter_Chances": 0x13911,
|
||||
"Option_Viridian_Gym_Badges": 0x1901d,
|
||||
"Event_Sleepy_Guy": 0x191bc,
|
||||
"Starter2_K": 0x195a8,
|
||||
"Starter3_K": 0x195b0,
|
||||
"Event_Rocket_Thief": 0x196cc,
|
||||
"Option_Cerulean_Cave_Condition": 0x1986c,
|
||||
"Event_Stranded_Man": 0x19b2b,
|
||||
"Event_Rivals_Sister": 0x19cf9,
|
||||
"Option_Pokemon_League_Badges": 0x19e16,
|
||||
"Missable_Silph_Co_4F_Item_1": 0x1a0d7,
|
||||
"Missable_Silph_Co_4F_Item_2": 0x1a0de,
|
||||
"Missable_Silph_Co_4F_Item_3": 0x1a0e5,
|
||||
"Missable_Silph_Co_5F_Item_1": 0x1a337,
|
||||
"Missable_Silph_Co_5F_Item_2": 0x1a33e,
|
||||
"Missable_Silph_Co_5F_Item_3": 0x1a345,
|
||||
"Missable_Silph_Co_6F_Item_1": 0x1a5ad,
|
||||
"Missable_Silph_Co_6F_Item_2": 0x1a5b4,
|
||||
"Event_Free_Sample": 0x1cade,
|
||||
"Starter1_F": 0x1cca5,
|
||||
"Starter2_F": 0x1cca9,
|
||||
"Starter2_G": 0x1cde2,
|
||||
"Starter3_G": 0x1cdea,
|
||||
"Starter2_H": 0x1d0e5,
|
||||
"Starter1_H": 0x1d0ef,
|
||||
"Starter3_I": 0x1d0f6,
|
||||
"Starter2_I": 0x1d100,
|
||||
"Starter1_D": 0x1d107,
|
||||
"Starter3_D": 0x1d111,
|
||||
"Starter2_E": 0x1d2eb,
|
||||
"Starter3_E": 0x1d2f3,
|
||||
"Event_Oaks_Gift": 0x1d373,
|
||||
"Event_Pokemart_Quest": 0x1d566,
|
||||
"Event_Bicycle_Shop": 0x1d805,
|
||||
"Text_Bicycle": 0x1d898,
|
||||
"Event_Fuji": 0x1d9cd,
|
||||
"Static_Encounter_Mew": 0x1dc4e,
|
||||
"Gift_Eevee": 0x1dcc7,
|
||||
"Event_Mr_Psychic": 0x1ddcf,
|
||||
"Static_Encounter_Voltorb_A": 0x1e397,
|
||||
"Static_Encounter_Voltorb_B": 0x1e39f,
|
||||
"Static_Encounter_Voltorb_C": 0x1e3a7,
|
||||
"Static_Encounter_Electrode_A": 0x1e3af,
|
||||
"Static_Encounter_Voltorb_D": 0x1e3b7,
|
||||
"Static_Encounter_Voltorb_E": 0x1e3bf,
|
||||
"Static_Encounter_Electrode_B": 0x1e3c7,
|
||||
"Static_Encounter_Voltorb_F": 0x1e3cf,
|
||||
"Static_Encounter_Zapdos": 0x1e3d7,
|
||||
"Missable_Power_Plant_Item_1": 0x1e3df,
|
||||
"Missable_Power_Plant_Item_2": 0x1e3e6,
|
||||
"Missable_Power_Plant_Item_3": 0x1e3ed,
|
||||
"Missable_Power_Plant_Item_4": 0x1e3f4,
|
||||
"Missable_Power_Plant_Item_5": 0x1e3fb,
|
||||
"Event_Rt16_House_Woman": 0x1e5d4,
|
||||
"Option_Victory_Road_Badges": 0x1e6a5,
|
||||
"Event_Bill": 0x1e8d6,
|
||||
"Starter1_O": 0x372b0,
|
||||
"Starter2_O": 0x372b4,
|
||||
"Starter3_O": 0x372b8,
|
||||
"Base_Stats": 0x383de,
|
||||
"Starter3_C": 0x39cf2,
|
||||
"Starter1_C": 0x39cf8,
|
||||
"Trainer_Data": 0x39d99,
|
||||
"Rival_Starter2_A": 0x3a1e5,
|
||||
"Rival_Starter3_A": 0x3a1e8,
|
||||
"Rival_Starter1_A": 0x3a1eb,
|
||||
"Rival_Starter2_B": 0x3a1f1,
|
||||
"Rival_Starter3_B": 0x3a1f7,
|
||||
"Rival_Starter1_B": 0x3a1fd,
|
||||
"Rival_Starter2_C": 0x3a207,
|
||||
"Rival_Starter3_C": 0x3a211,
|
||||
"Rival_Starter1_C": 0x3a21b,
|
||||
"Rival_Starter2_D": 0x3a409,
|
||||
"Rival_Starter3_D": 0x3a413,
|
||||
"Rival_Starter1_D": 0x3a41d,
|
||||
"Rival_Starter2_E": 0x3a429,
|
||||
"Rival_Starter3_E": 0x3a435,
|
||||
"Rival_Starter1_E": 0x3a441,
|
||||
"Rival_Starter2_F": 0x3a44d,
|
||||
"Rival_Starter3_F": 0x3a459,
|
||||
"Rival_Starter1_F": 0x3a465,
|
||||
"Rival_Starter2_G": 0x3a473,
|
||||
"Rival_Starter3_G": 0x3a481,
|
||||
"Rival_Starter1_G": 0x3a48f,
|
||||
"Rival_Starter2_H": 0x3a49d,
|
||||
"Rival_Starter3_H": 0x3a4ab,
|
||||
"Rival_Starter1_H": 0x3a4b9,
|
||||
"Trainer_Data_End": 0x3a52e,
|
||||
"Learnset_Rhydon": 0x3b1d9,
|
||||
"Learnset_Kangaskhan": 0x3b1e7,
|
||||
"Learnset_NidoranM": 0x3b1f6,
|
||||
"Learnset_Clefairy": 0x3b208,
|
||||
"Learnset_Spearow": 0x3b219,
|
||||
"Learnset_Voltorb": 0x3b228,
|
||||
"Learnset_Nidoking": 0x3b234,
|
||||
"Learnset_Slowbro": 0x3b23c,
|
||||
"Learnset_Ivysaur": 0x3b24f,
|
||||
"Learnset_Exeggutor": 0x3b25f,
|
||||
"Learnset_Lickitung": 0x3b263,
|
||||
"Learnset_Exeggcute": 0x3b273,
|
||||
"Learnset_Grimer": 0x3b284,
|
||||
"Learnset_Gengar": 0x3b292,
|
||||
"Learnset_NidoranF": 0x3b29b,
|
||||
"Learnset_Nidoqueen": 0x3b2a9,
|
||||
"Learnset_Cubone": 0x3b2b4,
|
||||
"Learnset_Rhyhorn": 0x3b2c3,
|
||||
"Learnset_Lapras": 0x3b2d1,
|
||||
"Learnset_Mew": 0x3b2e1,
|
||||
"Learnset_Gyarados": 0x3b2eb,
|
||||
"Learnset_Shellder": 0x3b2fb,
|
||||
"Learnset_Tentacool": 0x3b30a,
|
||||
"Learnset_Gastly": 0x3b31f,
|
||||
"Learnset_Scyther": 0x3b325,
|
||||
"Learnset_Staryu": 0x3b337,
|
||||
"Learnset_Blastoise": 0x3b347,
|
||||
"Learnset_Pinsir": 0x3b355,
|
||||
"Learnset_Tangela": 0x3b363,
|
||||
"Learnset_Growlithe": 0x3b379,
|
||||
"Learnset_Onix": 0x3b385,
|
||||
"Learnset_Fearow": 0x3b391,
|
||||
"Learnset_Pidgey": 0x3b3a0,
|
||||
"Learnset_Slowpoke": 0x3b3b1,
|
||||
"Learnset_Kadabra": 0x3b3c9,
|
||||
"Learnset_Graveler": 0x3b3e1,
|
||||
"Learnset_Chansey": 0x3b3ef,
|
||||
"Learnset_Machoke": 0x3b407,
|
||||
"Learnset_MrMime": 0x3b413,
|
||||
"Learnset_Hitmonlee": 0x3b41f,
|
||||
"Learnset_Hitmonchan": 0x3b42b,
|
||||
"Learnset_Arbok": 0x3b437,
|
||||
"Learnset_Parasect": 0x3b443,
|
||||
"Learnset_Psyduck": 0x3b452,
|
||||
"Learnset_Drowzee": 0x3b461,
|
||||
"Learnset_Golem": 0x3b46f,
|
||||
"Learnset_Magmar": 0x3b47f,
|
||||
"Learnset_Electabuzz": 0x3b48f,
|
||||
"Learnset_Magneton": 0x3b49b,
|
||||
"Learnset_Koffing": 0x3b4ac,
|
||||
"Learnset_Mankey": 0x3b4bd,
|
||||
"Learnset_Seel": 0x3b4cc,
|
||||
"Learnset_Diglett": 0x3b4db,
|
||||
"Learnset_Tauros": 0x3b4e7,
|
||||
"Learnset_Farfetchd": 0x3b4f9,
|
||||
"Learnset_Venonat": 0x3b508,
|
||||
"Learnset_Dragonite": 0x3b516,
|
||||
"Learnset_Doduo": 0x3b52b,
|
||||
"Learnset_Poliwag": 0x3b53c,
|
||||
"Learnset_Jynx": 0x3b54a,
|
||||
"Learnset_Moltres": 0x3b558,
|
||||
"Learnset_Articuno": 0x3b560,
|
||||
"Learnset_Zapdos": 0x3b568,
|
||||
"Learnset_Meowth": 0x3b575,
|
||||
"Learnset_Krabby": 0x3b584,
|
||||
"Learnset_Vulpix": 0x3b59a,
|
||||
"Learnset_Pikachu": 0x3b5ac,
|
||||
"Learnset_Dratini": 0x3b5c1,
|
||||
"Learnset_Dragonair": 0x3b5d0,
|
||||
"Learnset_Kabuto": 0x3b5df,
|
||||
"Learnset_Kabutops": 0x3b5e9,
|
||||
"Learnset_Horsea": 0x3b5f6,
|
||||
"Learnset_Seadra": 0x3b602,
|
||||
"Learnset_Sandshrew": 0x3b615,
|
||||
"Learnset_Sandslash": 0x3b621,
|
||||
"Learnset_Omanyte": 0x3b630,
|
||||
"Learnset_Omastar": 0x3b63a,
|
||||
"Learnset_Jigglypuff": 0x3b648,
|
||||
"Learnset_Eevee": 0x3b666,
|
||||
"Learnset_Flareon": 0x3b670,
|
||||
"Learnset_Jolteon": 0x3b682,
|
||||
"Learnset_Vaporeon": 0x3b694,
|
||||
"Learnset_Machop": 0x3b6a9,
|
||||
"Learnset_Zubat": 0x3b6b8,
|
||||
"Learnset_Ekans": 0x3b6c7,
|
||||
"Learnset_Paras": 0x3b6d6,
|
||||
"Learnset_Poliwhirl": 0x3b6e6,
|
||||
"Learnset_Poliwrath": 0x3b6f4,
|
||||
"Learnset_Beedrill": 0x3b704,
|
||||
"Learnset_Dodrio": 0x3b714,
|
||||
"Learnset_Primeape": 0x3b722,
|
||||
"Learnset_Dugtrio": 0x3b72e,
|
||||
"Learnset_Venomoth": 0x3b73a,
|
||||
"Learnset_Dewgong": 0x3b748,
|
||||
"Learnset_Butterfree": 0x3b762,
|
||||
"Learnset_Machamp": 0x3b772,
|
||||
"Learnset_Golduck": 0x3b780,
|
||||
"Learnset_Hypno": 0x3b78c,
|
||||
"Learnset_Golbat": 0x3b79a,
|
||||
"Learnset_Mewtwo": 0x3b7a6,
|
||||
"Learnset_Snorlax": 0x3b7b2,
|
||||
"Learnset_Magikarp": 0x3b7bf,
|
||||
"Learnset_Muk": 0x3b7c7,
|
||||
"Learnset_Kingler": 0x3b7d7,
|
||||
"Learnset_Cloyster": 0x3b7e3,
|
||||
"Learnset_Electrode": 0x3b7e9,
|
||||
"Learnset_Weezing": 0x3b7f7,
|
||||
"Learnset_Persian": 0x3b803,
|
||||
"Learnset_Marowak": 0x3b80f,
|
||||
"Learnset_Haunter": 0x3b827,
|
||||
"Learnset_Alakazam": 0x3b832,
|
||||
"Learnset_Pidgeotto": 0x3b843,
|
||||
"Learnset_Pidgeot": 0x3b851,
|
||||
"Learnset_Bulbasaur": 0x3b864,
|
||||
"Learnset_Venusaur": 0x3b874,
|
||||
"Learnset_Tentacruel": 0x3b884,
|
||||
"Learnset_Goldeen": 0x3b89b,
|
||||
"Learnset_Seaking": 0x3b8a9,
|
||||
"Learnset_Ponyta": 0x3b8c2,
|
||||
"Learnset_Rapidash": 0x3b8d0,
|
||||
"Learnset_Rattata": 0x3b8e1,
|
||||
"Learnset_Raticate": 0x3b8eb,
|
||||
"Learnset_Nidorino": 0x3b8f9,
|
||||
"Learnset_Nidorina": 0x3b90b,
|
||||
"Learnset_Geodude": 0x3b91c,
|
||||
"Learnset_Porygon": 0x3b92a,
|
||||
"Learnset_Aerodactyl": 0x3b934,
|
||||
"Learnset_Magnemite": 0x3b942,
|
||||
"Learnset_Charmander": 0x3b957,
|
||||
"Learnset_Squirtle": 0x3b968,
|
||||
"Learnset_Charmeleon": 0x3b979,
|
||||
"Learnset_Wartortle": 0x3b98a,
|
||||
"Learnset_Charizard": 0x3b998,
|
||||
"Learnset_Oddish": 0x3b9b1,
|
||||
"Learnset_Gloom": 0x3b9c3,
|
||||
"Learnset_Vileplume": 0x3b9d1,
|
||||
"Learnset_Bellsprout": 0x3b9dc,
|
||||
"Learnset_Weepinbell": 0x3b9f0,
|
||||
"Learnset_Victreebel": 0x3ba00,
|
||||
"Type_Chart": 0x3e4b6,
|
||||
"Type_Chart_Divider": 0x3e5ac,
|
||||
"Ghost_Battle3": 0x3efd9,
|
||||
"Missable_Pokemon_Mansion_1F_Item_1": 0x443d6,
|
||||
"Missable_Pokemon_Mansion_1F_Item_2": 0x443dd,
|
||||
"Map_Rock_TunnelF": 0x44676,
|
||||
"Missable_Victory_Road_3F_Item_1": 0x44b07,
|
||||
"Missable_Victory_Road_3F_Item_2": 0x44b0e,
|
||||
"Missable_Rocket_Hideout_B1F_Item_1": 0x44d2d,
|
||||
"Missable_Rocket_Hideout_B1F_Item_2": 0x44d34,
|
||||
"Missable_Rocket_Hideout_B2F_Item_1": 0x4511d,
|
||||
"Missable_Rocket_Hideout_B2F_Item_2": 0x45124,
|
||||
"Missable_Rocket_Hideout_B2F_Item_3": 0x4512b,
|
||||
"Missable_Rocket_Hideout_B2F_Item_4": 0x45132,
|
||||
"Missable_Rocket_Hideout_B3F_Item_1": 0x4536f,
|
||||
"Missable_Rocket_Hideout_B3F_Item_2": 0x45376,
|
||||
"Missable_Rocket_Hideout_B4F_Item_1": 0x45627,
|
||||
"Missable_Rocket_Hideout_B4F_Item_2": 0x4562e,
|
||||
"Missable_Rocket_Hideout_B4F_Item_3": 0x45635,
|
||||
"Missable_Rocket_Hideout_B4F_Item_4": 0x4563c,
|
||||
"Missable_Rocket_Hideout_B4F_Item_5": 0x45643,
|
||||
"Missable_Safari_Zone_East_Item_1": 0x458b2,
|
||||
"Missable_Safari_Zone_East_Item_2": 0x458b9,
|
||||
"Missable_Safari_Zone_East_Item_3": 0x458c0,
|
||||
"Missable_Safari_Zone_East_Item_4": 0x458c7,
|
||||
"Missable_Safari_Zone_North_Item_1": 0x45a12,
|
||||
"Missable_Safari_Zone_North_Item_2": 0x45a19,
|
||||
"Missable_Safari_Zone_Center_Item": 0x45bf9,
|
||||
"Missable_Cerulean_Cave_2F_Item_1": 0x45e36,
|
||||
"Missable_Cerulean_Cave_2F_Item_2": 0x45e3d,
|
||||
"Missable_Cerulean_Cave_2F_Item_3": 0x45e44,
|
||||
"Static_Encounter_Mewtwo": 0x45f44,
|
||||
"Missable_Cerulean_Cave_B1F_Item_1": 0x45f4c,
|
||||
"Missable_Cerulean_Cave_B1F_Item_2": 0x45f53,
|
||||
"Missable_Rock_Tunnel_B1F_Item_1": 0x4619f,
|
||||
"Missable_Rock_Tunnel_B1F_Item_2": 0x461a6,
|
||||
"Missable_Rock_Tunnel_B1F_Item_3": 0x461ad,
|
||||
"Missable_Rock_Tunnel_B1F_Item_4": 0x461b4,
|
||||
"Static_Encounter_Articuno": 0x4690c,
|
||||
"Hidden_Item_Viridian_Forest_1": 0x46e6d,
|
||||
"Hidden_Item_Viridian_Forest_2": 0x46e73,
|
||||
"Hidden_Item_MtMoonB2F_1": 0x46e7a,
|
||||
"Hidden_Item_MtMoonB2F_2": 0x46e80,
|
||||
"Hidden_Item_Route_25_1": 0x46e94,
|
||||
"Hidden_Item_Route_25_2": 0x46e9a,
|
||||
"Hidden_Item_Route_9": 0x46ea1,
|
||||
"Hidden_Item_SS_Anne_Kitchen": 0x46eb4,
|
||||
"Hidden_Item_SS_Anne_B1F": 0x46ebb,
|
||||
"Hidden_Item_Route_10_1": 0x46ec2,
|
||||
"Hidden_Item_Route_10_2": 0x46ec8,
|
||||
"Hidden_Item_Rocket_Hideout_B1F": 0x46ecf,
|
||||
"Hidden_Item_Rocket_Hideout_B3F": 0x46ed6,
|
||||
"Hidden_Item_Rocket_Hideout_B4F": 0x46edd,
|
||||
"Hidden_Item_Pokemon_Tower_5F": 0x46ef1,
|
||||
"Hidden_Item_Route_13_1": 0x46ef8,
|
||||
"Hidden_Item_Route_13_2": 0x46efe,
|
||||
"Hidden_Item_Safari_Zone_West": 0x46f0c,
|
||||
"Hidden_Item_Silph_Co_5F": 0x46f13,
|
||||
"Hidden_Item_Silph_Co_9F": 0x46f1a,
|
||||
"Hidden_Item_Copycats_House": 0x46f21,
|
||||
"Hidden_Item_Cerulean_Cave_1F": 0x46f28,
|
||||
"Hidden_Item_Cerulean_Cave_B1F": 0x46f2f,
|
||||
"Hidden_Item_Power_Plant_1": 0x46f36,
|
||||
"Hidden_Item_Power_Plant_2": 0x46f3c,
|
||||
"Hidden_Item_Seafoam_Islands_B2F": 0x46f43,
|
||||
"Hidden_Item_Seafoam_Islands_B4F": 0x46f4a,
|
||||
"Hidden_Item_Pokemon_Mansion_1F": 0x46f51,
|
||||
"Hidden_Item_Pokemon_Mansion_3F": 0x46f65,
|
||||
"Hidden_Item_Pokemon_Mansion_B1F": 0x46f72,
|
||||
"Hidden_Item_Route_23_1": 0x46f85,
|
||||
"Hidden_Item_Route_23_2": 0x46f8b,
|
||||
"Hidden_Item_Route_23_3": 0x46f91,
|
||||
"Hidden_Item_Victory_Road_2F_1": 0x46f98,
|
||||
"Hidden_Item_Victory_Road_2F_2": 0x46f9e,
|
||||
"Hidden_Item_Unused_6F": 0x46fa5,
|
||||
"Hidden_Item_Viridian_City": 0x46fb3,
|
||||
"Hidden_Item_Route_11": 0x47060,
|
||||
"Hidden_Item_Route_12": 0x47067,
|
||||
"Hidden_Item_Route_17_1": 0x47075,
|
||||
"Hidden_Item_Route_17_2": 0x4707b,
|
||||
"Hidden_Item_Route_17_3": 0x47081,
|
||||
"Hidden_Item_Route_17_4": 0x47087,
|
||||
"Hidden_Item_Route_17_5": 0x4708d,
|
||||
"Hidden_Item_Underground_Path_NS_1": 0x47094,
|
||||
"Hidden_Item_Underground_Path_NS_2": 0x4709a,
|
||||
"Hidden_Item_Underground_Path_WE_1": 0x470a1,
|
||||
"Hidden_Item_Underground_Path_WE_2": 0x470a7,
|
||||
"Hidden_Item_Celadon_City": 0x470ae,
|
||||
"Hidden_Item_Seafoam_Islands_B3F": 0x470b5,
|
||||
"Hidden_Item_Vermilion_City": 0x470bc,
|
||||
"Hidden_Item_Cerulean_City": 0x470c3,
|
||||
"Hidden_Item_Route_4": 0x470ca,
|
||||
"Event_Counter": 0x482d3,
|
||||
"Event_Thirsty_Girl_Lemonade": 0x484f9,
|
||||
"Event_Thirsty_Girl_Soda": 0x4851d,
|
||||
"Event_Thirsty_Girl_Water": 0x48541,
|
||||
"Option_Tea": 0x4871d,
|
||||
"Event_Mansion_Lady": 0x4872a,
|
||||
"Badge_Celadon_Gym": 0x48a1b,
|
||||
"Event_Celadon_Gym": 0x48a2f,
|
||||
"Event_Gambling_Addict": 0x49293,
|
||||
"Gift_Magikarp": 0x49430,
|
||||
"Option_Aide_Rt11": 0x4958d,
|
||||
"Event_Rt11_Oaks_Aide": 0x49591,
|
||||
"Event_Mourning_Girl": 0x4968b,
|
||||
"Option_Aide_Rt15": 0x49776,
|
||||
"Event_Rt_15_Oaks_Aide": 0x4977a,
|
||||
"Missable_Mt_Moon_1F_Item_1": 0x49c75,
|
||||
"Missable_Mt_Moon_1F_Item_2": 0x49c7c,
|
||||
"Missable_Mt_Moon_1F_Item_3": 0x49c83,
|
||||
"Missable_Mt_Moon_1F_Item_4": 0x49c8a,
|
||||
"Missable_Mt_Moon_1F_Item_5": 0x49c91,
|
||||
"Missable_Mt_Moon_1F_Item_6": 0x49c98,
|
||||
"Dome_Fossil_Text": 0x4a001,
|
||||
"Event_Dome_Fossil": 0x4a021,
|
||||
"Helix_Fossil_Text": 0x4a05d,
|
||||
"Event_Helix_Fossil": 0x4a07d,
|
||||
"Missable_Mt_Moon_B2F_Item_1": 0x4a166,
|
||||
"Missable_Mt_Moon_B2F_Item_2": 0x4a16d,
|
||||
"Missable_Safari_Zone_West_Item_1": 0x4a34f,
|
||||
"Missable_Safari_Zone_West_Item_2": 0x4a356,
|
||||
"Missable_Safari_Zone_West_Item_3": 0x4a35d,
|
||||
"Missable_Safari_Zone_West_Item_4": 0x4a364,
|
||||
"Event_Safari_Zone_Secret_House": 0x4a469,
|
||||
"Missable_Route_24_Item": 0x506e6,
|
||||
"Missable_Route_25_Item": 0x5080b,
|
||||
"Starter2_B": 0x50fce,
|
||||
"Starter3_B": 0x50fd0,
|
||||
"Starter1_B": 0x50fd2,
|
||||
"Starter2_A": 0x510f1,
|
||||
"Starter3_A": 0x510f3,
|
||||
"Starter1_A": 0x510f5,
|
||||
"Option_Badge_Goal": 0x51317,
|
||||
"Event_Nugget_Bridge": 0x5148f,
|
||||
"Static_Encounter_Moltres": 0x51939,
|
||||
"Missable_Victory_Road_2F_Item_1": 0x51941,
|
||||
"Missable_Victory_Road_2F_Item_2": 0x51948,
|
||||
"Missable_Victory_Road_2F_Item_3": 0x5194f,
|
||||
"Missable_Victory_Road_2F_Item_4": 0x51956,
|
||||
"Starter2_L": 0x51c85,
|
||||
"Starter3_L": 0x51c8d,
|
||||
"Gift_Lapras": 0x51d83,
|
||||
"Missable_Silph_Co_7F_Item_1": 0x51f0d,
|
||||
"Missable_Silph_Co_7F_Item_2": 0x51f14,
|
||||
"Missable_Pokemon_Mansion_2F_Item": 0x520c9,
|
||||
"Missable_Pokemon_Mansion_3F_Item_1": 0x522e2,
|
||||
"Missable_Pokemon_Mansion_3F_Item_2": 0x522e9,
|
||||
"Missable_Pokemon_Mansion_B1F_Item_1": 0x5248c,
|
||||
"Missable_Pokemon_Mansion_B1F_Item_2": 0x52493,
|
||||
"Missable_Pokemon_Mansion_B1F_Item_3": 0x5249a,
|
||||
"Missable_Pokemon_Mansion_B1F_Item_4": 0x524a1,
|
||||
"Missable_Pokemon_Mansion_B1F_Item_5": 0x524ae,
|
||||
"Option_Safari_Zone_Battle_Type": 0x525c3,
|
||||
"Prize_Mon_A2": 0x5282f,
|
||||
"Prize_Mon_B2": 0x52830,
|
||||
"Prize_Mon_C2": 0x52831,
|
||||
"Prize_Mon_D2": 0x5283a,
|
||||
"Prize_Mon_E2": 0x5283b,
|
||||
"Prize_Mon_F2": 0x5283c,
|
||||
"Prize_Mon_A": 0x52960,
|
||||
"Prize_Mon_B": 0x52962,
|
||||
"Prize_Mon_C": 0x52964,
|
||||
"Prize_Mon_D": 0x52966,
|
||||
"Prize_Mon_E": 0x52968,
|
||||
"Prize_Mon_F": 0x5296a,
|
||||
"Missable_Route_2_Item_1": 0x5404a,
|
||||
"Missable_Route_2_Item_2": 0x54051,
|
||||
"Missable_Route_4_Item": 0x543df,
|
||||
"Missable_Route_9_Item": 0x546fd,
|
||||
"Option_EXP_Modifier": 0x552c5,
|
||||
"Rod_Vermilion_City_Fishing_Guru": 0x560df,
|
||||
"Rod_Fuchsia_City_Fishing_Brother": 0x561eb,
|
||||
"Rod_Route12_Fishing_Brother": 0x564ee,
|
||||
"Missable_Route_12_Item_1": 0x58704,
|
||||
"Missable_Route_12_Item_2": 0x5870b,
|
||||
"Missable_Route_15_Item": 0x589c7,
|
||||
"Ghost_Battle6": 0x58df0,
|
||||
"Static_Encounter_Snorlax_A": 0x5969b,
|
||||
"Static_Encounter_Snorlax_B": 0x599db,
|
||||
"Event_Pokemon_Fan_Club": 0x59c8b,
|
||||
"Event_Scared_Woman": 0x59e1f,
|
||||
"Missable_Silph_Co_3F_Item": 0x5a0cb,
|
||||
"Missable_Silph_Co_10F_Item_1": 0x5a281,
|
||||
"Missable_Silph_Co_10F_Item_2": 0x5a288,
|
||||
"Missable_Silph_Co_10F_Item_3": 0x5a28f,
|
||||
"Guard_Drink_List": 0x5a600,
|
||||
"Event_Museum": 0x5c266,
|
||||
"Badge_Pewter_Gym": 0x5c3ed,
|
||||
"Event_Pewter_Gym": 0x5c401,
|
||||
"Badge_Cerulean_Gym": 0x5c716,
|
||||
"Event_Cerulean_Gym": 0x5c72a,
|
||||
"Badge_Vermilion_Gym": 0x5caba,
|
||||
"Event_Vermillion_Gym": 0x5cace,
|
||||
"Event_Copycat": 0x5cca9,
|
||||
"Gift_Hitmonlee": 0x5cf1a,
|
||||
"Gift_Hitmonchan": 0x5cf62,
|
||||
"Badge_Saffron_Gym": 0x5d079,
|
||||
"Event_Saffron_Gym": 0x5d08d,
|
||||
"Option_Aide_Rt2": 0x5d5f2,
|
||||
"Event_Route_2_Oaks_Aide": 0x5d5f6,
|
||||
"Missable_Victory_Road_1F_Item_1": 0x5dae6,
|
||||
"Missable_Victory_Road_1F_Item_2": 0x5daed,
|
||||
"Starter2_J": 0x6060e,
|
||||
"Starter3_J": 0x60616,
|
||||
"Missable_Pokemon_Tower_3F_Item": 0x60787,
|
||||
"Missable_Pokemon_Tower_4F_Item_1": 0x608b5,
|
||||
"Missable_Pokemon_Tower_4F_Item_2": 0x608bc,
|
||||
"Missable_Pokemon_Tower_4F_Item_3": 0x608c3,
|
||||
"Missable_Pokemon_Tower_5F_Item": 0x60a80,
|
||||
"Ghost_Battle1": 0x60b33,
|
||||
"Ghost_Battle2": 0x60c0a,
|
||||
"Missable_Pokemon_Tower_6F_Item_1": 0x60c85,
|
||||
"Missable_Pokemon_Tower_6F_Item_2": 0x60c8c,
|
||||
"Gift_Aerodactyl": 0x61064,
|
||||
"Gift_Omanyte": 0x61068,
|
||||
"Gift_Kabuto": 0x6106c,
|
||||
"Missable_Viridian_Forest_Item_1": 0x6122c,
|
||||
"Missable_Viridian_Forest_Item_2": 0x61233,
|
||||
"Missable_Viridian_Forest_Item_3": 0x6123a,
|
||||
"Starter2_M": 0x61450,
|
||||
"Starter3_M": 0x61458,
|
||||
"Event_SS_Anne_Captain": 0x618c3,
|
||||
"Missable_SS_Anne_1F_Item": 0x61ac0,
|
||||
"Missable_SS_Anne_2F_Item_1": 0x61ced,
|
||||
"Missable_SS_Anne_2F_Item_2": 0x61d00,
|
||||
"Missable_SS_Anne_B1F_Item_1": 0x61ee3,
|
||||
"Missable_SS_Anne_B1F_Item_2": 0x61eea,
|
||||
"Missable_SS_Anne_B1F_Item_3": 0x61ef1,
|
||||
"Event_Silph_Co_President": 0x622ed,
|
||||
"Ghost_Battle4": 0x708e1,
|
||||
"Badge_Viridian_Gym": 0x749ca,
|
||||
"Event_Viridian_Gym": 0x749de,
|
||||
"Missable_Viridian_Gym_Item": 0x74c63,
|
||||
"Missable_Cerulean_Cave_1F_Item_1": 0x74d68,
|
||||
"Missable_Cerulean_Cave_1F_Item_2": 0x74d6f,
|
||||
"Missable_Cerulean_Cave_1F_Item_3": 0x74d76,
|
||||
"Event_Warden": 0x7512a,
|
||||
"Missable_Wardens_House_Item": 0x751b7,
|
||||
"Badge_Fuchsia_Gym": 0x755cd,
|
||||
"Event_Fuschia_Gym": 0x755e1,
|
||||
"Badge_Cinnabar_Gym": 0x75995,
|
||||
"Event_Cinnabar_Gym": 0x759a9,
|
||||
"Event_Lab_Scientist": 0x75dd6,
|
||||
"Fossils_Needed_For_Second_Item": 0x75ea3,
|
||||
"Event_Dome_Fossil_B": 0x75f20,
|
||||
"Event_Helix_Fossil_B": 0x75f40,
|
||||
"Starter2_N": 0x76169,
|
||||
"Starter3_N": 0x76171,
|
||||
"Option_Itemfinder": 0x76864,
|
||||
"Text_Badges_Needed": 0x92304,
|
||||
"Badge_Text_Boulder_Badge": 0x990b3,
|
||||
"Badge_Text_Cascade_Badge": 0x990cb,
|
||||
"Badge_Text_Thunder_Badge": 0x99111,
|
||||
"Badge_Text_Rainbow_Badge": 0x9912e,
|
||||
"Badge_Text_Soul_Badge": 0x99177,
|
||||
"Badge_Text_Marsh_Badge": 0x9918c,
|
||||
"Badge_Text_Volcano_Badge": 0x991d6,
|
||||
"Badge_Text_Earth_Badge": 0x991f3,
|
||||
}
|
||||
@@ -0,0 +1,165 @@
|
||||
from ..generic.Rules import add_item_rule, add_rule
|
||||
|
||||
def set_rules(world, player):
|
||||
|
||||
add_item_rule(world.get_location("Pallet Town - Player's PC", player),
|
||||
lambda i: i.player == player and "Badge" not in i.name)
|
||||
|
||||
access_rules = {
|
||||
|
||||
"Pallet Town - Rival's Sister": lambda state: state.has("Oak's Parcel", player),
|
||||
"Pallet Town - Oak's Post-Route-22-Rival Gift": lambda state: state.has("Oak's Parcel", player),
|
||||
"Viridian City - Sleepy Guy": lambda state: state.pokemon_rb_can_cut(player) or state.pokemon_rb_can_surf(player),
|
||||
"Route 2 - Oak's Aide": lambda state: state.pokemon_rb_has_pokemon(state.world.oaks_aide_rt_2[player].value + 5, player),
|
||||
"Pewter City - Museum": lambda state: state.pokemon_rb_can_cut(player),
|
||||
"Cerulean City - Bicycle Shop": lambda state: state.has("Bike Voucher", player),
|
||||
"Lavender Town - Mr. Fuji": lambda state: state.has("Fuji Saved", player),
|
||||
"Vermilion Gym - Lt. Surge 1": lambda state: state.pokemon_rb_can_cut(player or state.pokemon_rb_can_surf(player)),
|
||||
"Vermilion Gym - Lt. Surge 2": lambda state: state.pokemon_rb_can_cut(player or state.pokemon_rb_can_surf(player)),
|
||||
"Route 11 - Oak's Aide": lambda state: state.pokemon_rb_has_pokemon(state.world.oaks_aide_rt_11[player].value + 5, player),
|
||||
"Celadon City - Stranded Man": lambda state: state.pokemon_rb_can_surf(player),
|
||||
"Silph Co 11F - Silph Co President": lambda state: state.has("Card Key", player),
|
||||
"Fuchsia City - Safari Zone Warden": lambda state: state.has("Gold Teeth", player),
|
||||
"Route 12 - Island Item": lambda state: state.pokemon_rb_can_surf(player),
|
||||
"Route 12 - Item Behind Cuttable Tree": lambda state: state.pokemon_rb_can_cut(player),
|
||||
"Route 15 - Item": lambda state: state.pokemon_rb_can_cut(player),
|
||||
"Route 25 - Item": lambda state: state.pokemon_rb_can_cut(player),
|
||||
"Fuchsia City - Warden's House Item": lambda state: state.pokemon_rb_can_strength(player),
|
||||
"Rocket Hideout B4F - Southwest Item (Lift Key)": lambda state: state.has("Lift Key", player),
|
||||
"Rocket Hideout B4F - Giovanni Item (Lift Key)": lambda state: state.has("Lift Key", player),
|
||||
"Silph Co 3F - Item (Card Key)": lambda state: state.has("Card Key", player),
|
||||
"Silph Co 4F - Left Item (Card Key)": lambda state: state.has("Card Key", player),
|
||||
"Silph Co 4F - Middle Item (Card Key)": lambda state: state.has("Card Key", player),
|
||||
"Silph Co 4F - Right Item (Card Key)": lambda state: state.has("Card Key", player),
|
||||
"Silph Co 5F - Northwest Item (Card Key)": lambda state: state.has("Card Key", player),
|
||||
"Silph Co 6F - West Item (Card Key)": lambda state: state.has("Card Key", player),
|
||||
"Silph Co 6F - Southwest Item (Card Key)": lambda state: state.has("Card Key", player),
|
||||
"Silph Co 7F - East Item (Card Key)": lambda state: state.has("Card Key", player),
|
||||
"Safari Zone Center - Island Item": lambda state: state.pokemon_rb_can_surf(player),
|
||||
|
||||
"Silph Co 11F - Silph Co Liberated": lambda state: state.has("Card Key", player),
|
||||
|
||||
"Pallet Town - Super Rod Pokemon - 1": lambda state: state.has("Super Rod", player),
|
||||
"Pallet Town - Super Rod Pokemon - 2": lambda state: state.has("Super Rod", player),
|
||||
"Route 22 - Super Rod Pokemon - 1": lambda state: state.has("Super Rod", player),
|
||||
"Route 22 - Super Rod Pokemon - 2": lambda state: state.has("Super Rod", player),
|
||||
"Route 24 - Super Rod Pokemon - 1": lambda state: state.has("Super Rod", player),
|
||||
"Route 24 - Super Rod Pokemon - 2": lambda state: state.has("Super Rod", player),
|
||||
"Route 24 - Super Rod Pokemon - 3": lambda state: state.has("Super Rod", player),
|
||||
"Route 6 - Super Rod Pokemon - 1": lambda state: state.has("Super Rod", player),
|
||||
"Route 6 - Super Rod Pokemon - 2": lambda state: state.has("Super Rod", player),
|
||||
"Route 10 - Super Rod Pokemon - 1": lambda state: state.has("Super Rod", player),
|
||||
"Route 10 - Super Rod Pokemon - 2": lambda state: state.has("Super Rod", player),
|
||||
"Safari Zone Center - Super Rod Pokemon - 1": lambda state: state.has("Super Rod", player),
|
||||
"Safari Zone Center - Super Rod Pokemon - 2": lambda state: state.has("Super Rod", player),
|
||||
"Safari Zone Center - Super Rod Pokemon - 3": lambda state: state.has("Super Rod", player),
|
||||
"Safari Zone Center - Super Rod Pokemon - 4": lambda state: state.has("Super Rod", player),
|
||||
"Route 12 - Super Rod Pokemon - 1": lambda state: state.has("Super Rod", player),
|
||||
"Route 12 - Super Rod Pokemon - 2": lambda state: state.has("Super Rod", player),
|
||||
"Route 12 - Super Rod Pokemon - 3": lambda state: state.has("Super Rod", player),
|
||||
"Route 12 - Super Rod Pokemon - 4": lambda state: state.has("Super Rod", player),
|
||||
"Route 19 - Super Rod Pokemon - 1": lambda state: state.has("Super Rod", player),
|
||||
"Route 19 - Super Rod Pokemon - 2": lambda state: state.has("Super Rod", player),
|
||||
"Route 19 - Super Rod Pokemon - 3": lambda state: state.has("Super Rod", player),
|
||||
"Route 19 - Super Rod Pokemon - 4": lambda state: state.has("Super Rod", player),
|
||||
"Route 23 - Super Rod Pokemon - 1": lambda state: state.has("Super Rod", player),
|
||||
"Route 23 - Super Rod Pokemon - 2": lambda state: state.has("Super Rod", player),
|
||||
"Route 23 - Super Rod Pokemon - 3": lambda state: state.has("Super Rod", player),
|
||||
"Route 23 - Super Rod Pokemon - 4": lambda state: state.has("Super Rod", player),
|
||||
"Fuchsia City - Super Rod Pokemon - 1": lambda state: state.has("Super Rod", player),
|
||||
"Fuchsia City - Super Rod Pokemon - 2": lambda state: state.has("Super Rod", player),
|
||||
"Fuchsia City - Super Rod Pokemon - 3": lambda state: state.has("Super Rod", player),
|
||||
"Fuchsia City - Super Rod Pokemon - 4": lambda state: state.has("Super Rod", player),
|
||||
"Anywhere - Good Rod Pokemon - 1": lambda state: state.has("Good Rod", player),
|
||||
"Anywhere - Good Rod Pokemon - 2": lambda state: state.has("Good Rod", player),
|
||||
"Anywhere - Old Rod Pokemon": lambda state: state.has("Old Rod", player),
|
||||
"Celadon Prize Corner - Pokemon Prize - 1": lambda state: state.has("Coin Case", player),
|
||||
"Celadon Prize Corner - Pokemon Prize - 2": lambda state: state.has("Coin Case", player),
|
||||
"Celadon Prize Corner - Pokemon Prize - 3": lambda state: state.has("Coin Case", player),
|
||||
"Celadon Prize Corner - Pokemon Prize - 4": lambda state: state.has("Coin Case", player),
|
||||
"Celadon Prize Corner - Pokemon Prize - 5": lambda state: state.has("Coin Case", player),
|
||||
"Celadon Prize Corner - Pokemon Prize - 6": lambda state: state.has("Coin Case", player),
|
||||
"Cinnabar Island - Old Amber Pokemon": lambda state: state.has("Old Amber", player),
|
||||
"Cinnabar Island - Helix Fossil Pokemon": lambda state: state.has("Helix Fossil", player),
|
||||
"Cinnabar Island - Dome Fossil Pokemon": lambda state: state.has("Dome Fossil", player),
|
||||
"Route 12 - Sleeping Pokemon": lambda state: state.has("Poke Flute", player),
|
||||
"Route 16 - Sleeping Pokemon": lambda state: state.has("Poke Flute", player),
|
||||
"Seafoam Islands B4F - Legendary Pokemon": lambda state: state.pokemon_rb_can_strength(player),
|
||||
"Vermilion City - Legendary Pokemon": lambda state: state.pokemon_rb_can_surf(player) and state.has("S.S. Ticket", player)
|
||||
}
|
||||
|
||||
hidden_item_access_rules = {
|
||||
"Viridian Forest - Hidden Item Northwest by Trainer": lambda state: state.pokemon_rb_can_get_hidden_items(
|
||||
player),
|
||||
"Viridian Forest - Hidden Item Entrance Tree": lambda state: state.pokemon_rb_can_get_hidden_items(player),
|
||||
"Mt Moon B2F - Hidden Item Dead End Before Fossils": lambda state: state.pokemon_rb_can_get_hidden_items(
|
||||
player),
|
||||
"Route 25 - Hidden Item Fence Outside Bill's House": lambda state: state.pokemon_rb_can_get_hidden_items(
|
||||
player),
|
||||
"Route 9 - Hidden Item Rock By Grass": lambda state: state.pokemon_rb_can_get_hidden_items(player),
|
||||
"S.S. Anne 1F - Hidden Item Kitchen Trash": lambda state: state.pokemon_rb_can_get_hidden_items(player),
|
||||
"S.S. Anne B1F - Hidden Item Under Pillow": lambda state: state.pokemon_rb_can_get_hidden_items(player),
|
||||
"Route 10 - Hidden Item Behind Rock Tunnel Entrance Tree": lambda
|
||||
state: state.pokemon_rb_can_get_hidden_items(player),
|
||||
"Route 10 - Hidden Item Rock": lambda state: state.pokemon_rb_can_get_hidden_items(player),
|
||||
"Rocket Hideout B1F - Hidden Item Pot Plant": lambda state: state.pokemon_rb_can_get_hidden_items(player),
|
||||
"Rocket Hideout B3F - Hidden Item Near East Item": lambda state: state.pokemon_rb_can_get_hidden_items(player),
|
||||
"Rocket Hideout B4F - Hidden Item Behind Giovanni": lambda state: state.pokemon_rb_can_get_hidden_items(player),
|
||||
"Pokemon Tower 5F - Hidden Item Near West Staircase": lambda state: state.pokemon_rb_can_get_hidden_items(
|
||||
player),
|
||||
"Route 13 - Hidden Item Dead End Boulder": lambda state: state.pokemon_rb_can_get_hidden_items(player),
|
||||
"Route 13 - Hidden Item Dead End By Water Corner": lambda state: state.pokemon_rb_can_get_hidden_items(player),
|
||||
"Pokemon Mansion B1F - Hidden Item Secret Key Room Corner": lambda state: state.pokemon_rb_can_get_hidden_items(
|
||||
player),
|
||||
"Safari Zone West - Hidden Item Secret House Statue": lambda state: state.pokemon_rb_can_get_hidden_items(
|
||||
player),
|
||||
"Silph Co 5F - Hidden Item Pot Plant": lambda state: state.pokemon_rb_can_get_hidden_items(player),
|
||||
"Silph Co 9F - Hidden Item Nurse Bed": lambda state: state.pokemon_rb_can_get_hidden_items(player),
|
||||
"Copycat's House - Hidden Item Desk": lambda state: state.pokemon_rb_can_get_hidden_items(player),
|
||||
"Cerulean Cave 1F - Hidden Item Center Rocks": lambda state: state.pokemon_rb_can_get_hidden_items(player),
|
||||
"Cerulean Cave B1F - Hidden Item Northeast Rocks": lambda state: state.pokemon_rb_can_get_hidden_items(player),
|
||||
"Power Plant - Hidden Item Central Dead End": lambda state: state.pokemon_rb_can_get_hidden_items(player),
|
||||
"Power Plant - Hidden Item Before Zapdos": lambda state: state.pokemon_rb_can_get_hidden_items(player),
|
||||
"Seafoam Islands B2F - Hidden Item Rock": lambda state: state.pokemon_rb_can_get_hidden_items(player),
|
||||
"Seafoam Islands B4F - Hidden Item Corner Island": lambda state: state.pokemon_rb_can_get_hidden_items(player),
|
||||
"Pokemon Mansion 1F - Hidden Item Block Near Entrance Carpet": lambda
|
||||
state: state.pokemon_rb_can_get_hidden_items(player),
|
||||
"Pokemon Mansion 3F - Hidden Item Behind Burglar": lambda state: state.pokemon_rb_can_get_hidden_items(player),
|
||||
"Route 23 - Hidden Item Rocks Before Final Guard": lambda state: state.pokemon_rb_can_get_hidden_items(
|
||||
player),
|
||||
"Route 23 - Hidden Item East Tree After Water": lambda state: state.pokemon_rb_can_get_hidden_items(
|
||||
player),
|
||||
"Route 23 - Hidden Item On Island": lambda state: state.pokemon_rb_can_get_hidden_items(player),
|
||||
"Victory Road 2F - Hidden Item Rock Before Moltres": lambda state: state.pokemon_rb_can_get_hidden_items(
|
||||
player),
|
||||
"Victory Road 2F - Hidden Item Rock In Final Room": lambda state: state.pokemon_rb_can_get_hidden_items(player),
|
||||
"Viridian City - Hidden Item Cuttable Tree": lambda state: state.pokemon_rb_can_get_hidden_items(player),
|
||||
"Route 11 - Hidden Item Isolated Tree Near Gate": lambda state: state.pokemon_rb_can_get_hidden_items(player),
|
||||
"Route 12 - Hidden Item Tree Near Gate": lambda state: state.pokemon_rb_can_get_hidden_items(player),
|
||||
"Route 17 - Hidden Item In Grass": lambda state: state.pokemon_rb_can_get_hidden_items(player),
|
||||
"Route 17 - Hidden Item Near Northernmost Sign": lambda state: state.pokemon_rb_can_get_hidden_items(player),
|
||||
"Route 17 - Hidden Item East Center": lambda state: state.pokemon_rb_can_get_hidden_items(player),
|
||||
"Route 17 - Hidden Item West Center": lambda state: state.pokemon_rb_can_get_hidden_items(player),
|
||||
"Route 17 - Hidden Item Before Final Bridge": lambda state: state.pokemon_rb_can_get_hidden_items(player),
|
||||
"Underground Tunnel North-South - Hidden Item Near Northern Stairs": lambda
|
||||
state: state.pokemon_rb_can_get_hidden_items(player),
|
||||
"Underground Tunnel North-South - Hidden Item Near Southern Stairs": lambda
|
||||
state: state.pokemon_rb_can_get_hidden_items(player),
|
||||
"Underground Tunnel West-East - Hidden Item West": lambda state: state.pokemon_rb_can_get_hidden_items(player),
|
||||
"Underground Tunnel West-East - Hidden Item East": lambda state: state.pokemon_rb_can_get_hidden_items(player),
|
||||
"Celadon City - Hidden Item Dead End Near Cuttable Tree": lambda state: state.pokemon_rb_can_get_hidden_items(
|
||||
player),
|
||||
"Route 25 - Hidden Item Northeast Of Grass": lambda state: state.pokemon_rb_can_get_hidden_items(player),
|
||||
"Mt Moon B2F - Hidden Item Lone Rock": lambda state: state.pokemon_rb_can_get_hidden_items(player),
|
||||
"Seafoam Islands B3F - Hidden Item Rock": lambda state: state.pokemon_rb_can_get_hidden_items(player),
|
||||
"Vermilion City - Hidden Item In Water Near Fan Club": lambda state: state.pokemon_rb_can_get_hidden_items(
|
||||
player),
|
||||
"Cerulean City - Hidden Item Gym Badge Guy's Backyard": lambda state: state.pokemon_rb_can_get_hidden_items(
|
||||
player),
|
||||
"Route 4 - Hidden Item Plateau East Of Mt Moon": lambda state: state.pokemon_rb_can_get_hidden_items(player),
|
||||
}
|
||||
for loc, rule in access_rules.items():
|
||||
add_rule(world.get_location(loc, player), rule)
|
||||
if world.randomize_hidden_items[player].value != 0:
|
||||
for loc, rule in hidden_item_access_rules.items():
|
||||
add_rule(world.get_location(loc, player), rule)
|
||||
@@ -0,0 +1,147 @@
|
||||
special_chars = {
|
||||
"PKMN": 0x4A,
|
||||
"'d": 0xBB,
|
||||
"'l": 0xBC,
|
||||
"'t": 0xBE,
|
||||
"'v": 0xBF,
|
||||
"PK": 0xE1,
|
||||
"MN": 0xE2,
|
||||
"'r": 0xE4,
|
||||
"'m": 0xE5,
|
||||
"MALE": 0xEF,
|
||||
"FEMALE": 0xF5,
|
||||
}
|
||||
|
||||
char_map = {
|
||||
"@": 0x50, # String terminator
|
||||
"#": 0x54, # Poké
|
||||
"‘": 0x70,
|
||||
"’": 0x71,
|
||||
"“": 0x72,
|
||||
"”": 0x73,
|
||||
"·": 0x74,
|
||||
"…": 0x75,
|
||||
" ": 0x7F,
|
||||
"A": 0x80,
|
||||
"B": 0x81,
|
||||
"C": 0x82,
|
||||
"D": 0x83,
|
||||
"E": 0x84,
|
||||
"F": 0x85,
|
||||
"G": 0x86,
|
||||
"H": 0x87,
|
||||
"I": 0x88,
|
||||
"J": 0x89,
|
||||
"K": 0x8A,
|
||||
"L": 0x8B,
|
||||
"M": 0x8C,
|
||||
"N": 0x8D,
|
||||
"O": 0x8E,
|
||||
"P": 0x8F,
|
||||
"Q": 0x90,
|
||||
"R": 0x91,
|
||||
"S": 0x92,
|
||||
"T": 0x93,
|
||||
"U": 0x94,
|
||||
"V": 0x95,
|
||||
"W": 0x96,
|
||||
"X": 0x97,
|
||||
"Y": 0x98,
|
||||
"Z": 0x99,
|
||||
"(": 0x9A,
|
||||
")": 0x9B,
|
||||
":": 0x9C,
|
||||
";": 0x9D,
|
||||
"[": 0x9E,
|
||||
"]": 0x9F,
|
||||
"a": 0xA0,
|
||||
"b": 0xA1,
|
||||
"c": 0xA2,
|
||||
"d": 0xA3,
|
||||
"e": 0xA4,
|
||||
"f": 0xA5,
|
||||
"g": 0xA6,
|
||||
"h": 0xA7,
|
||||
"i": 0xA8,
|
||||
"j": 0xA9,
|
||||
"k": 0xAA,
|
||||
"l": 0xAB,
|
||||
"m": 0xAC,
|
||||
"n": 0xAD,
|
||||
"o": 0xAE,
|
||||
"p": 0xAF,
|
||||
"q": 0xB0,
|
||||
"r": 0xB1,
|
||||
"s": 0xB2,
|
||||
"t": 0xB3,
|
||||
"u": 0xB4,
|
||||
"v": 0xB5,
|
||||
"w": 0xB6,
|
||||
"x": 0xB7,
|
||||
"y": 0xB8,
|
||||
"z": 0xB9,
|
||||
"é": 0xBA,
|
||||
"'": 0xE0,
|
||||
"-": 0xE3,
|
||||
"?": 0xE6,
|
||||
"!": 0xE7,
|
||||
".": 0xE8,
|
||||
"♂": 0xEF,
|
||||
"¥": 0xF0,
|
||||
"$": 0xF0,
|
||||
"×": 0xF1,
|
||||
"/": 0xF3,
|
||||
",": 0xF4,
|
||||
"♀": 0xF5,
|
||||
"0": 0xF6,
|
||||
"1": 0xF7,
|
||||
"2": 0xF8,
|
||||
"3": 0xF9,
|
||||
"4": 0xFA,
|
||||
"5": 0xFB,
|
||||
"6": 0xFC,
|
||||
"7": 0xFD,
|
||||
"8": 0xFE,
|
||||
"9": 0xFF,
|
||||
}
|
||||
|
||||
unsafe_chars = ["@", "#", "PKMN"]
|
||||
|
||||
|
||||
def encode_text(text: str, length: int=0, whitespace=False, force=False, safety=False):
|
||||
encoded_text = bytearray()
|
||||
spec_char = ""
|
||||
special = False
|
||||
for char in text:
|
||||
if char == ">":
|
||||
if spec_char in unsafe_chars and safety:
|
||||
raise KeyError(f"Disallowed Pokemon text special character '<{spec_char}>'")
|
||||
try:
|
||||
encoded_text.append(special_chars[spec_char])
|
||||
except KeyError:
|
||||
if force:
|
||||
encoded_text.append(char_map[" "])
|
||||
else:
|
||||
raise KeyError(f"Invalid Pokemon text special character '<{spec_char}>'")
|
||||
spec_char = ""
|
||||
special = False
|
||||
elif char == "<":
|
||||
spec_char = ""
|
||||
special = True
|
||||
elif special is True:
|
||||
spec_char += char
|
||||
else:
|
||||
if char in unsafe_chars and safety:
|
||||
raise KeyError(f"Disallowed Pokemon text character '{char}'")
|
||||
try:
|
||||
encoded_text.append(char_map[char])
|
||||
except KeyError:
|
||||
if force:
|
||||
encoded_text.append(char_map[" "])
|
||||
else:
|
||||
raise KeyError(f"Invalid Pokemon text character '{char}'")
|
||||
if length > 0:
|
||||
encoded_text = encoded_text[:length]
|
||||
while whitespace and len(encoded_text) < length:
|
||||
encoded_text.append(char_map[" " if whitespace is True else whitespace])
|
||||
return encoded_text
|
||||
@@ -98,37 +98,37 @@ class RaftLogic(LogicMixin):
|
||||
return self.raft_can_access_vasagatan(player)
|
||||
|
||||
def raft_can_access_balboa_island(self, player):
|
||||
return self.raft_can_drive(player) and self.has("Balboa Island Frequency", player)
|
||||
return self.raft_can_navigate(player) and self.raft_can_drive(player) and self.has("Balboa Island Frequency", player)
|
||||
|
||||
def raft_can_complete_balboa_island(self, player):
|
||||
return self.raft_can_access_balboa_island(player) and self.raft_can_craft_machete(player)
|
||||
|
||||
def raft_can_access_caravan_island(self, player):
|
||||
return self.raft_can_drive(player) and self.has("Caravan Island Frequency", player)
|
||||
return self.raft_can_navigate(player) and self.raft_can_drive(player) and self.has("Caravan Island Frequency", player)
|
||||
|
||||
def raft_can_complete_caravan_island(self, player):
|
||||
return self.raft_can_access_caravan_island(player) and self.raft_can_craft_ziplineTool(player)
|
||||
|
||||
def raft_can_access_tangaroa(self, player):
|
||||
return self.raft_can_drive(player) and self.has("Tangaroa Frequency", player)
|
||||
return self.raft_can_navigate(player) and self.raft_can_drive(player) and self.has("Tangaroa Frequency", player)
|
||||
|
||||
def raft_can_complete_tangaroa(self, player):
|
||||
return self.raft_can_access_tangaroa(player) and self.raft_can_craft_ziplineTool(player)
|
||||
|
||||
def raft_can_access_varuna_point(self, player):
|
||||
return self.raft_can_drive(player) and self.has("Varuna Point Frequency", player)
|
||||
return self.raft_can_navigate(player) and self.raft_can_drive(player) and self.has("Varuna Point Frequency", player)
|
||||
|
||||
def raft_can_complete_varuna_point(self, player):
|
||||
return self.raft_can_access_varuna_point(player) and self.raft_can_craft_ziplineTool(player)
|
||||
|
||||
def raft_can_access_temperance(self, player):
|
||||
return self.raft_can_drive(player) and self.has("Temperance Frequency", player)
|
||||
return self.raft_can_navigate(player) and self.raft_can_drive(player) and self.has("Temperance Frequency", player)
|
||||
|
||||
def raft_can_complete_temperance(self, player):
|
||||
return self.raft_can_access_temperance(player) # No zipline required on Temperance
|
||||
|
||||
def raft_can_access_utopia(self, player):
|
||||
return (self.raft_can_drive(player)
|
||||
return (self.raft_can_navigate(player) and self.raft_can_drive(player)
|
||||
# Access checks are to prevent frequencies for other
|
||||
# islands from appearing in Utopia
|
||||
and self.raft_can_access_radio_tower(player)
|
||||
|
||||
@@ -66,7 +66,7 @@ class LegacyWorld(World):
|
||||
|
||||
def _create_items(self, name: str):
|
||||
data = item_table[name]
|
||||
return [self.create_item(name)] * data.quantity
|
||||
return [self.create_item(name) for _ in range(data.quantity)]
|
||||
|
||||
def fill_slot_data(self) -> dict:
|
||||
slot_data = self._get_slot_data()
|
||||
@@ -89,20 +89,20 @@ class LegacyWorld(World):
|
||||
|
||||
# Blueprints
|
||||
if self.world.progressive_blueprints[self.player]:
|
||||
itempool += [self.create_item(ItemName.progressive_blueprints)] * 15
|
||||
itempool += [self.create_item(ItemName.progressive_blueprints) for _ in range(15)]
|
||||
else:
|
||||
for item in blueprints_table:
|
||||
itempool += self._create_items(item)
|
||||
|
||||
# Check Pool settings to add a certain amount of these items.
|
||||
itempool += [self.create_item(ItemName.health)] * int(self.world.health_pool[self.player])
|
||||
itempool += [self.create_item(ItemName.mana)] * int(self.world.mana_pool[self.player])
|
||||
itempool += [self.create_item(ItemName.attack)] * int(self.world.attack_pool[self.player])
|
||||
itempool += [self.create_item(ItemName.magic_damage)] * int(self.world.magic_damage_pool[self.player])
|
||||
itempool += [self.create_item(ItemName.armor)] * int(self.world.armor_pool[self.player])
|
||||
itempool += [self.create_item(ItemName.equip)] * int(self.world.equip_pool[self.player])
|
||||
itempool += [self.create_item(ItemName.crit_chance)] * int(self.world.crit_chance_pool[self.player])
|
||||
itempool += [self.create_item(ItemName.crit_damage)] * int(self.world.crit_damage_pool[self.player])
|
||||
itempool += [self.create_item(ItemName.health) for _ in range(self.world.health_pool[self.player])]
|
||||
itempool += [self.create_item(ItemName.mana) for _ in range(self.world.mana_pool[self.player])]
|
||||
itempool += [self.create_item(ItemName.attack) for _ in range(self.world.attack_pool[self.player])]
|
||||
itempool += [self.create_item(ItemName.magic_damage) for _ in range(self.world.magic_damage_pool[self.player])]
|
||||
itempool += [self.create_item(ItemName.armor) for _ in range(self.world.armor_pool[self.player])]
|
||||
itempool += [self.create_item(ItemName.equip) for _ in range(self.world.equip_pool[self.player])]
|
||||
itempool += [self.create_item(ItemName.crit_chance) for _ in range(self.world.crit_chance_pool[self.player])]
|
||||
itempool += [self.create_item(ItemName.crit_damage) for _ in range(self.world.crit_damage_pool[self.player])]
|
||||
|
||||
classes = self.world.available_classes[self.player]
|
||||
if "Dragon" in classes:
|
||||
@@ -153,12 +153,12 @@ class LegacyWorld(World):
|
||||
if self.world.architect[self.player] == "start_unlocked":
|
||||
self.world.push_precollected(self.world.create_item(ItemName.architect, self.player))
|
||||
elif self.world.architect[self.player] != "disabled":
|
||||
itempool += [self.create_item(ItemName.architect)]
|
||||
itempool.append(self.create_item(ItemName.architect))
|
||||
|
||||
# Fill item pool with the remaining
|
||||
for _ in range(len(itempool), total_required_locations):
|
||||
item = self.world.random.choice(list(misc_items_table.keys()))
|
||||
itempool += [self.create_item(item)]
|
||||
itempool.append(self.create_item(item))
|
||||
|
||||
self.world.itempool += itempool
|
||||
|
||||
|
||||
@@ -86,7 +86,7 @@ class SA2BWorld(World):
|
||||
|
||||
def _create_items(self, name: str):
|
||||
data = item_table[name]
|
||||
return [self.create_item(name)] * data.quantity
|
||||
return [self.create_item(name) for _ in range(data.quantity)]
|
||||
|
||||
def fill_slot_data(self) -> dict:
|
||||
slot_data = self._get_slot_data()
|
||||
@@ -198,11 +198,11 @@ class SA2BWorld(World):
|
||||
connect_regions(self.world, self.player, gates, self.emblems_for_cannons_core, self.gate_bosses)
|
||||
|
||||
max_required_emblems = max(max(emblem_requirement_list), self.emblems_for_cannons_core)
|
||||
itempool += [self.create_item(ItemName.emblem)] * max_required_emblems
|
||||
itempool += [self.create_item(ItemName.emblem) for _ in range(max_required_emblems)]
|
||||
|
||||
non_required_emblems = (total_emblem_count - max_required_emblems)
|
||||
junk_count = math.floor(non_required_emblems * (self.world.junk_fill_percentage[self.player].value / 100.0))
|
||||
itempool += [self.create_item(ItemName.emblem, True)] * (non_required_emblems - junk_count)
|
||||
itempool += [self.create_item(ItemName.emblem, True) for _ in range(non_required_emblems - junk_count)]
|
||||
|
||||
# Carve Traps out of junk_count
|
||||
trap_weights = []
|
||||
@@ -219,14 +219,14 @@ class SA2BWorld(World):
|
||||
junk_keys = list(junk_table.keys())
|
||||
for i in range(junk_count):
|
||||
junk_item = self.world.random.choice(junk_keys)
|
||||
junk_pool += [self.create_item(junk_item)]
|
||||
junk_pool.append(self.create_item(junk_item))
|
||||
|
||||
itempool += junk_pool
|
||||
|
||||
trap_pool = []
|
||||
for i in range(trap_count):
|
||||
trap_item = self.world.random.choice(trap_weights)
|
||||
trap_pool += [self.create_item(trap_item)]
|
||||
trap_pool.append(self.create_item(trap_item))
|
||||
|
||||
itempool += trap_pool
|
||||
|
||||
|
||||
+89
-51
@@ -1,5 +1,7 @@
|
||||
from BaseClasses import Item, ItemClassification
|
||||
from BaseClasses import Item, ItemClassification, MultiWorld
|
||||
import typing
|
||||
|
||||
from .Options import get_option_value
|
||||
from .MissionTables import vanilla_mission_req_table
|
||||
|
||||
|
||||
@@ -9,6 +11,7 @@ class ItemData(typing.NamedTuple):
|
||||
number: typing.Optional[int]
|
||||
classification: ItemClassification = ItemClassification.useful
|
||||
quantity: int = 1
|
||||
parent_item: str = None
|
||||
|
||||
|
||||
class StarcraftWoLItem(Item):
|
||||
@@ -48,51 +51,51 @@ item_table = {
|
||||
"Progressive Ship Weapon": ItemData(105 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 8, quantity=3),
|
||||
"Progressive Ship Armor": ItemData(106 + SC2WOL_ITEM_ID_OFFSET, "Upgrade", 10, quantity=3),
|
||||
|
||||
"Projectile Accelerator (Bunker)": ItemData(200 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 0),
|
||||
"Neosteel Bunker (Bunker)": ItemData(201 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 1),
|
||||
"Titanium Housing (Missile Turret)": ItemData(202 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 2, classification=ItemClassification.filler),
|
||||
"Hellstorm Batteries (Missile Turret)": ItemData(203 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 3),
|
||||
"Advanced Construction (SCV)": ItemData(204 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 4),
|
||||
"Dual-Fusion Welders (SCV)": ItemData(205 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 5),
|
||||
"Projectile Accelerator (Bunker)": ItemData(200 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 0, parent_item="Bunker"),
|
||||
"Neosteel Bunker (Bunker)": ItemData(201 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 1, parent_item="Bunker"),
|
||||
"Titanium Housing (Missile Turret)": ItemData(202 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 2, classification=ItemClassification.filler, parent_item="Missile Turret"),
|
||||
"Hellstorm Batteries (Missile Turret)": ItemData(203 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 3, parent_item="Missile Turret"),
|
||||
"Advanced Construction (SCV)": ItemData(204 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 4, parent_item="SCV"),
|
||||
"Dual-Fusion Welders (SCV)": ItemData(205 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 5, parent_item="SCV"),
|
||||
"Fire-Suppression System (Building)": ItemData(206 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 6, classification=ItemClassification.filler),
|
||||
"Orbital Command (Building)": ItemData(207 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 7),
|
||||
"Stimpack (Marine)": ItemData(208 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 8),
|
||||
"Combat Shield (Marine)": ItemData(209 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 9, classification=ItemClassification.progression),
|
||||
"Advanced Medic Facilities (Medic)": ItemData(210 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 10, classification=ItemClassification.progression),
|
||||
"Stabilizer Medpacks (Medic)": ItemData(211 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 11, classification=ItemClassification.progression),
|
||||
"Incinerator Gauntlets (Firebat)": ItemData(212 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 12, classification=ItemClassification.filler),
|
||||
"Juggernaut Plating (Firebat)": ItemData(213 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 13),
|
||||
"Concussive Shells (Marauder)": ItemData(214 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 14),
|
||||
"Kinetic Foam (Marauder)": ItemData(215 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 15),
|
||||
"U-238 Rounds (Reaper)": ItemData(216 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 16),
|
||||
"G-4 Clusterbomb (Reaper)": ItemData(217 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 17, classification=ItemClassification.progression),
|
||||
"Stimpack (Marine)": ItemData(208 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 8, parent_item="Marine"),
|
||||
"Combat Shield (Marine)": ItemData(209 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 9, classification=ItemClassification.progression, parent_item="Marine"),
|
||||
"Advanced Medic Facilities (Medic)": ItemData(210 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 10, classification=ItemClassification.progression, parent_item="Medic"),
|
||||
"Stabilizer Medpacks (Medic)": ItemData(211 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 11, classification=ItemClassification.progression, parent_item="Medic"),
|
||||
"Incinerator Gauntlets (Firebat)": ItemData(212 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 12, classification=ItemClassification.filler, parent_item="Firebat"),
|
||||
"Juggernaut Plating (Firebat)": ItemData(213 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 13, parent_item="Firebat"),
|
||||
"Concussive Shells (Marauder)": ItemData(214 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 14, parent_item="Marauder"),
|
||||
"Kinetic Foam (Marauder)": ItemData(215 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 15, parent_item="Marauder"),
|
||||
"U-238 Rounds (Reaper)": ItemData(216 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 16, parent_item="Reaper"),
|
||||
"G-4 Clusterbomb (Reaper)": ItemData(217 + SC2WOL_ITEM_ID_OFFSET, "Armory 1", 17, classification=ItemClassification.progression, parent_item="Reaper"),
|
||||
|
||||
"Twin-Linked Flamethrower (Hellion)": ItemData(300 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 0, classification=ItemClassification.filler),
|
||||
"Thermite Filaments (Hellion)": ItemData(301 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 1),
|
||||
"Cerberus Mine (Vulture)": ItemData(302 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 2, classification=ItemClassification.filler),
|
||||
"Replenishable Magazine (Vulture)": ItemData(303 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 3, classification=ItemClassification.filler),
|
||||
"Multi-Lock Weapons System (Goliath)": ItemData(304 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 4),
|
||||
"Ares-Class Targeting System (Goliath)": ItemData(305 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 5),
|
||||
"Tri-Lithium Power Cell (Diamondback)": ItemData(306 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 6, classification=ItemClassification.filler),
|
||||
"Shaped Hull (Diamondback)": ItemData(307 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 7, classification=ItemClassification.filler),
|
||||
"Maelstrom Rounds (Siege Tank)": ItemData(308 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 8),
|
||||
"Shaped Blast (Siege Tank)": ItemData(309 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 9),
|
||||
"Rapid Deployment Tube (Medivac)": ItemData(310 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 10, classification=ItemClassification.filler),
|
||||
"Advanced Healing AI (Medivac)": ItemData(311 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 11, classification=ItemClassification.filler),
|
||||
"Tomahawk Power Cells (Wraith)": ItemData(312 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 12, classification=ItemClassification.filler),
|
||||
"Displacement Field (Wraith)": ItemData(313 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 13, classification=ItemClassification.filler),
|
||||
"Ripwave Missiles (Viking)": ItemData(314 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 14),
|
||||
"Phobos-Class Weapons System (Viking)": ItemData(315 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 15),
|
||||
"Cross-Spectrum Dampeners (Banshee)": ItemData(316 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 16, classification=ItemClassification.filler),
|
||||
"Shockwave Missile Battery (Banshee)": ItemData(317 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 17),
|
||||
"Missile Pods (Battlecruiser)": ItemData(318 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 18, classification=ItemClassification.filler),
|
||||
"Defensive Matrix (Battlecruiser)": ItemData(319 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 19, classification=ItemClassification.filler),
|
||||
"Ocular Implants (Ghost)": ItemData(320 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 20),
|
||||
"Crius Suit (Ghost)": ItemData(321 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 21),
|
||||
"Psionic Lash (Spectre)": ItemData(322 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 22, classification=ItemClassification.progression),
|
||||
"Nyx-Class Cloaking Module (Spectre)": ItemData(323 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 23),
|
||||
"330mm Barrage Cannon (Thor)": ItemData(324 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 24, classification=ItemClassification.filler),
|
||||
"Immortality Protocol (Thor)": ItemData(325 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 25, classification=ItemClassification.filler),
|
||||
"Twin-Linked Flamethrower (Hellion)": ItemData(300 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 0, classification=ItemClassification.filler, parent_item="Hellion"),
|
||||
"Thermite Filaments (Hellion)": ItemData(301 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 1, parent_item="Hellion"),
|
||||
"Cerberus Mine (Vulture)": ItemData(302 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 2, classification=ItemClassification.filler, parent_item="Vulture"),
|
||||
"Replenishable Magazine (Vulture)": ItemData(303 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 3, classification=ItemClassification.filler, parent_item="Vulture"),
|
||||
"Multi-Lock Weapons System (Goliath)": ItemData(304 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 4, parent_item="Goliath"),
|
||||
"Ares-Class Targeting System (Goliath)": ItemData(305 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 5, parent_item="Goliath"),
|
||||
"Tri-Lithium Power Cell (Diamondback)": ItemData(306 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 6, classification=ItemClassification.filler, parent_item="Diamondback"),
|
||||
"Shaped Hull (Diamondback)": ItemData(307 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 7, classification=ItemClassification.filler, parent_item="Diamondback"),
|
||||
"Maelstrom Rounds (Siege Tank)": ItemData(308 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 8, classification=ItemClassification.progression, parent_item="Siege Tank"),
|
||||
"Shaped Blast (Siege Tank)": ItemData(309 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 9, parent_item="Siege Tank"),
|
||||
"Rapid Deployment Tube (Medivac)": ItemData(310 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 10, classification=ItemClassification.filler, parent_item="Medivac"),
|
||||
"Advanced Healing AI (Medivac)": ItemData(311 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 11, classification=ItemClassification.filler, parent_item="Medivac"),
|
||||
"Tomahawk Power Cells (Wraith)": ItemData(312 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 12, classification=ItemClassification.filler, parent_item="Wraith"),
|
||||
"Displacement Field (Wraith)": ItemData(313 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 13, classification=ItemClassification.filler, parent_item="Wraith"),
|
||||
"Ripwave Missiles (Viking)": ItemData(314 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 14, parent_item="Viking"),
|
||||
"Phobos-Class Weapons System (Viking)": ItemData(315 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 15, parent_item="Viking"),
|
||||
"Cross-Spectrum Dampeners (Banshee)": ItemData(316 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 16, classification=ItemClassification.filler, parent_item="Banshee"),
|
||||
"Shockwave Missile Battery (Banshee)": ItemData(317 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 17, parent_item="Banshee"),
|
||||
"Missile Pods (Battlecruiser)": ItemData(318 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 18, classification=ItemClassification.filler, parent_item="Battlecruiser"),
|
||||
"Defensive Matrix (Battlecruiser)": ItemData(319 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 19, classification=ItemClassification.filler, parent_item="Battlecruiser"),
|
||||
"Ocular Implants (Ghost)": ItemData(320 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 20, parent_item="Ghost"),
|
||||
"Crius Suit (Ghost)": ItemData(321 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 21, parent_item="Ghost"),
|
||||
"Psionic Lash (Spectre)": ItemData(322 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 22, classification=ItemClassification.progression, parent_item="Spectre"),
|
||||
"Nyx-Class Cloaking Module (Spectre)": ItemData(323 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 23, parent_item="Spectre"),
|
||||
"330mm Barrage Cannon (Thor)": ItemData(324 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 24, classification=ItemClassification.filler, parent_item="Thor"),
|
||||
"Immortality Protocol (Thor)": ItemData(325 + SC2WOL_ITEM_ID_OFFSET, "Armory 2", 25, classification=ItemClassification.filler, parent_item="Thor"),
|
||||
|
||||
"Bunker": ItemData(400 + SC2WOL_ITEM_ID_OFFSET, "Building", 0, classification=ItemClassification.progression),
|
||||
"Missile Turret": ItemData(401 + SC2WOL_ITEM_ID_OFFSET, "Building", 1, classification=ItemClassification.progression),
|
||||
@@ -117,16 +120,16 @@ item_table = {
|
||||
"Science Vessel": ItemData(607 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 7, classification=ItemClassification.progression),
|
||||
"Tech Reactor": ItemData(608 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 8),
|
||||
"Orbital Strike": ItemData(609 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 9),
|
||||
"Shrike Turret": ItemData(610 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 10),
|
||||
"Fortified Bunker": ItemData(611 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 11),
|
||||
"Planetary Fortress": ItemData(612 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 12),
|
||||
"Perdition Turret": ItemData(613 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 13),
|
||||
"Shrike Turret": ItemData(610 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 10, parent_item="Bunker"),
|
||||
"Fortified Bunker": ItemData(611 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 11, parent_item="Bunker"),
|
||||
"Planetary Fortress": ItemData(612 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 12, classification=ItemClassification.progression),
|
||||
"Perdition Turret": ItemData(613 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 13, classification=ItemClassification.progression),
|
||||
"Predator": ItemData(614 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 14, classification=ItemClassification.filler),
|
||||
"Hercules": ItemData(615 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 15, classification=ItemClassification.progression),
|
||||
"Cellular Reactor": ItemData(616 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 16, classification=ItemClassification.filler),
|
||||
"Regenerative Bio-Steel": ItemData(617 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 17, classification=ItemClassification.filler),
|
||||
"Hive Mind Emulator": ItemData(618 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 18),
|
||||
"Psi Disrupter": ItemData(619 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 19, classification=ItemClassification.filler),
|
||||
"Hive Mind Emulator": ItemData(618 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 18, ItemClassification.progression),
|
||||
"Psi Disrupter": ItemData(619 + SC2WOL_ITEM_ID_OFFSET, "Laboratory", 19, classification=ItemClassification.progression),
|
||||
|
||||
"Zealot": ItemData(700 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 0, classification=ItemClassification.progression),
|
||||
"Stalker": ItemData(701 + SC2WOL_ITEM_ID_OFFSET, "Protoss", 1, classification=ItemClassification.progression),
|
||||
@@ -141,15 +144,33 @@ item_table = {
|
||||
"+15 Starting Minerals": ItemData(800 + SC2WOL_ITEM_ID_OFFSET, "Minerals", 15, quantity=0, classification=ItemClassification.filler),
|
||||
"+15 Starting Vespene": ItemData(801 + SC2WOL_ITEM_ID_OFFSET, "Vespene", 15, quantity=0, classification=ItemClassification.filler),
|
||||
"+2 Starting Supply": ItemData(802 + SC2WOL_ITEM_ID_OFFSET, "Supply", 2, quantity=0, classification=ItemClassification.filler),
|
||||
|
||||
# "Keystone Piece": ItemData(850 + SC2WOL_ITEM_ID_OFFSET, "Goal", 0, quantity=0, classification=ItemClassification.progression_skip_balancing)
|
||||
}
|
||||
|
||||
basic_unit: typing.Tuple[str, ...] = (
|
||||
|
||||
basic_units = {
|
||||
'Marine',
|
||||
'Marauder',
|
||||
'Firebat',
|
||||
'Hellion',
|
||||
'Vulture'
|
||||
)
|
||||
}
|
||||
|
||||
advanced_basic_units = {
|
||||
'Reaper',
|
||||
'Goliath',
|
||||
'Diamondback',
|
||||
'Viking'
|
||||
}
|
||||
|
||||
|
||||
def get_basic_units(world: MultiWorld, player: int) -> typing.Set[str]:
|
||||
if get_option_value(world, player, 'required_tactics') > 0:
|
||||
return basic_units.union(advanced_basic_units)
|
||||
else:
|
||||
return basic_units
|
||||
|
||||
|
||||
item_name_groups = {}
|
||||
for item, data in item_table.items():
|
||||
@@ -161,6 +182,22 @@ filler_items: typing.Tuple[str, ...] = (
|
||||
'+15 Starting Vespene'
|
||||
)
|
||||
|
||||
defense_ratings = {
|
||||
"Siege Tank": 5,
|
||||
"Maelstrom Rounds": 2,
|
||||
"Planetary Fortress": 3,
|
||||
# Bunker w/ Marine/Marauder: 3,
|
||||
"Perdition Turret": 2,
|
||||
"Missile Turret": 2,
|
||||
"Vulture": 2
|
||||
}
|
||||
zerg_defense_ratings = {
|
||||
"Perdition Turret": 2,
|
||||
# Bunker w/ Firebat
|
||||
"Hive Mind Emulator": 3,
|
||||
"Psi Disruptor": 3
|
||||
}
|
||||
|
||||
lookup_id_to_name: typing.Dict[int, str] = {data.code: item_name for item_name, data in get_full_item_list().items() if
|
||||
data.code}
|
||||
# Map type to expected int
|
||||
@@ -176,4 +213,5 @@ type_flaggroups: typing.Dict[str, int] = {
|
||||
"Minerals": 8,
|
||||
"Vespene": 9,
|
||||
"Supply": 10,
|
||||
"Goal": 11
|
||||
}
|
||||
|
||||
+86
-119
@@ -1,5 +1,6 @@
|
||||
from typing import List, Tuple, Optional, Callable, NamedTuple
|
||||
from BaseClasses import MultiWorld
|
||||
from .Options import get_option_value
|
||||
|
||||
from BaseClasses import Location
|
||||
|
||||
@@ -19,6 +20,7 @@ class LocationData(NamedTuple):
|
||||
|
||||
def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[LocationData, ...]:
|
||||
# Note: rules which are ended with or True are rules identified as needed later when restricted units is an option
|
||||
logic_level = get_option_value(world, player, 'required_tactics')
|
||||
location_table: List[LocationData] = [
|
||||
LocationData("Liberation Day", "Liberation Day: Victory", SC2WOL_LOC_ID_OFFSET + 100),
|
||||
LocationData("Liberation Day", "Liberation Day: First Statue", SC2WOL_LOC_ID_OFFSET + 101),
|
||||
@@ -27,40 +29,38 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
|
||||
LocationData("Liberation Day", "Liberation Day: Fourth Statue", SC2WOL_LOC_ID_OFFSET + 104),
|
||||
LocationData("Liberation Day", "Liberation Day: Fifth Statue", SC2WOL_LOC_ID_OFFSET + 105),
|
||||
LocationData("Liberation Day", "Liberation Day: Sixth Statue", SC2WOL_LOC_ID_OFFSET + 106),
|
||||
LocationData("Liberation Day", "Beat Liberation Day", None),
|
||||
LocationData("The Outlaws", "The Outlaws: Victory", SC2WOL_LOC_ID_OFFSET + 200,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player)),
|
||||
LocationData("The Outlaws", "The Outlaws: Rebel Base", SC2WOL_LOC_ID_OFFSET + 201,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player)),
|
||||
LocationData("The Outlaws", "Beat The Outlaws", None,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player)),
|
||||
LocationData("Zero Hour", "Zero Hour: Victory", SC2WOL_LOC_ID_OFFSET + 300,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player)),
|
||||
lambda state: state._sc2wol_has_common_unit(world, player) and
|
||||
state._sc2wol_defense_rating(world, player, True) >= 2 and
|
||||
(logic_level > 0 or state._sc2wol_has_anti_air(world, player))),
|
||||
LocationData("Zero Hour", "Zero Hour: First Group Rescued", SC2WOL_LOC_ID_OFFSET + 301),
|
||||
LocationData("Zero Hour", "Zero Hour: Second Group Rescued", SC2WOL_LOC_ID_OFFSET + 302,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player)),
|
||||
LocationData("Zero Hour", "Zero Hour: Third Group Rescued", SC2WOL_LOC_ID_OFFSET + 303,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player)),
|
||||
LocationData("Zero Hour", "Beat Zero Hour", None,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player)),
|
||||
lambda state: state._sc2wol_has_common_unit(world, player) and
|
||||
state._sc2wol_defense_rating(world, player, True) >= 2),
|
||||
LocationData("Evacuation", "Evacuation: Victory", SC2WOL_LOC_ID_OFFSET + 400,
|
||||
lambda state: state._sc2wol_has_anti_air(world, player)),
|
||||
lambda state: state._sc2wol_has_common_unit(world, player) and
|
||||
(logic_level > 0 and state._sc2wol_has_anti_air(world, player)
|
||||
or state._sc2wol_has_competent_anti_air(world, player))),
|
||||
LocationData("Evacuation", "Evacuation: First Chysalis", SC2WOL_LOC_ID_OFFSET + 401),
|
||||
LocationData("Evacuation", "Evacuation: Second Chysalis", SC2WOL_LOC_ID_OFFSET + 402,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player)),
|
||||
LocationData("Evacuation", "Evacuation: Third Chysalis", SC2WOL_LOC_ID_OFFSET + 403,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player)),
|
||||
LocationData("Evacuation", "Beat Evacuation", None,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player) and
|
||||
state._sc2wol_has_competent_anti_air(world, player)),
|
||||
LocationData("Outbreak", "Outbreak: Victory", SC2WOL_LOC_ID_OFFSET + 500,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)),
|
||||
lambda state: state._sc2wol_defense_rating(world, player, True, False) >= 4 and
|
||||
(state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player))),
|
||||
LocationData("Outbreak", "Outbreak: Left Infestor", SC2WOL_LOC_ID_OFFSET + 501,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)),
|
||||
lambda state: state._sc2wol_defense_rating(world, player, True, False) >= 2 and
|
||||
(state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player))),
|
||||
LocationData("Outbreak", "Outbreak: Right Infestor", SC2WOL_LOC_ID_OFFSET + 502,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)),
|
||||
LocationData("Outbreak", "Beat Outbreak", None,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)),
|
||||
lambda state: state._sc2wol_defense_rating(world, player, True, False) >= 2 and
|
||||
(state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player))),
|
||||
LocationData("Safe Haven", "Safe Haven: Victory", SC2WOL_LOC_ID_OFFSET + 600,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player) and
|
||||
state._sc2wol_has_competent_anti_air(world, player)),
|
||||
@@ -73,53 +73,50 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
|
||||
LocationData("Safe Haven", "Safe Haven: South Nexus", SC2WOL_LOC_ID_OFFSET + 603,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player) and
|
||||
state._sc2wol_has_competent_anti_air(world, player)),
|
||||
LocationData("Safe Haven", "Beat Safe Haven", None,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player) and
|
||||
state._sc2wol_has_competent_anti_air(world, player)),
|
||||
LocationData("Haven's Fall", "Haven's Fall: Victory", SC2WOL_LOC_ID_OFFSET + 700,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player) and
|
||||
state._sc2wol_has_competent_anti_air(world, player)),
|
||||
state._sc2wol_has_competent_anti_air(world, player) and
|
||||
state._sc2wol_defense_rating(world, player, True) >= 3),
|
||||
LocationData("Haven's Fall", "Haven's Fall: North Hive", SC2WOL_LOC_ID_OFFSET + 701,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player) and
|
||||
state._sc2wol_has_competent_anti_air(world, player)),
|
||||
state._sc2wol_has_competent_anti_air(world, player) and
|
||||
state._sc2wol_defense_rating(world, player, True) >= 3),
|
||||
LocationData("Haven's Fall", "Haven's Fall: East Hive", SC2WOL_LOC_ID_OFFSET + 702,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player) and
|
||||
state._sc2wol_has_competent_anti_air(world, player)),
|
||||
state._sc2wol_has_competent_anti_air(world, player) and
|
||||
state._sc2wol_defense_rating(world, player, True) >= 3),
|
||||
LocationData("Haven's Fall", "Haven's Fall: South Hive", SC2WOL_LOC_ID_OFFSET + 703,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player) and
|
||||
state._sc2wol_has_competent_anti_air(world, player)),
|
||||
LocationData("Haven's Fall", "Beat Haven's Fall", None,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player) and
|
||||
state._sc2wol_has_competent_anti_air(world, player)),
|
||||
state._sc2wol_has_competent_anti_air(world, player) and
|
||||
state._sc2wol_defense_rating(world, player, True) >= 3),
|
||||
LocationData("Smash and Grab", "Smash and Grab: Victory", SC2WOL_LOC_ID_OFFSET + 800,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player) and
|
||||
state._sc2wol_has_competent_anti_air(world, player)),
|
||||
(logic_level > 0 and state._sc2wol_has_anti_air(world, player)
|
||||
or state._sc2wol_has_competent_anti_air(world, player))),
|
||||
LocationData("Smash and Grab", "Smash and Grab: First Relic", SC2WOL_LOC_ID_OFFSET + 801),
|
||||
LocationData("Smash and Grab", "Smash and Grab: Second Relic", SC2WOL_LOC_ID_OFFSET + 802),
|
||||
LocationData("Smash and Grab", "Smash and Grab: Third Relic", SC2WOL_LOC_ID_OFFSET + 803,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player)),
|
||||
lambda state: state._sc2wol_has_common_unit(world, player) and
|
||||
(logic_level > 0 and state._sc2wol_has_anti_air(world, player)
|
||||
or state._sc2wol_has_competent_anti_air(world, player))),
|
||||
LocationData("Smash and Grab", "Smash and Grab: Fourth Relic", SC2WOL_LOC_ID_OFFSET + 804,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player) and
|
||||
state._sc2wol_has_anti_air(world, player)),
|
||||
LocationData("Smash and Grab", "Beat Smash and Grab", None,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player) and
|
||||
state._sc2wol_has_anti_air(world, player)),
|
||||
(logic_level > 0 and state._sc2wol_has_anti_air(world, player)
|
||||
or state._sc2wol_has_competent_anti_air(world, player))),
|
||||
LocationData("The Dig", "The Dig: Victory", SC2WOL_LOC_ID_OFFSET + 900,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player) and
|
||||
state._sc2wol_has_anti_air(world, player) and
|
||||
state._sc2wol_has_heavy_defense(world, player)),
|
||||
lambda state: state._sc2wol_has_anti_air(world, player) and
|
||||
state._sc2wol_defense_rating(world, player, False) >= 7),
|
||||
LocationData("The Dig", "The Dig: Left Relic", SC2WOL_LOC_ID_OFFSET + 901,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player)),
|
||||
lambda state: state._sc2wol_defense_rating(world, player, False) >= 5),
|
||||
LocationData("The Dig", "The Dig: Right Ground Relic", SC2WOL_LOC_ID_OFFSET + 902,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player)),
|
||||
lambda state: state._sc2wol_defense_rating(world, player, False) >= 5),
|
||||
LocationData("The Dig", "The Dig: Right Cliff Relic", SC2WOL_LOC_ID_OFFSET + 903,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player)),
|
||||
LocationData("The Dig", "Beat The Dig", None,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player) and
|
||||
state._sc2wol_has_anti_air(world, player) and
|
||||
state._sc2wol_has_heavy_defense(world, player)),
|
||||
lambda state: state._sc2wol_defense_rating(world, player, False) >= 5),
|
||||
LocationData("The Moebius Factor", "The Moebius Factor: Victory", SC2WOL_LOC_ID_OFFSET + 1000,
|
||||
lambda state: state._sc2wol_has_air(world, player) and state._sc2wol_has_anti_air(world, player)),
|
||||
lambda state: state._sc2wol_has_anti_air(world, player) and
|
||||
(state._sc2wol_has_air(world, player)
|
||||
or state.has_any({'Medivac', 'Hercules'}, player)
|
||||
and state._sc2wol_has_common_unit(world, player))),
|
||||
LocationData("The Moebius Factor", "The Moebius Factor: South Rescue", SC2WOL_LOC_ID_OFFSET + 1003,
|
||||
lambda state: state._sc2wol_able_to_rescue(world, player)),
|
||||
LocationData("The Moebius Factor", "The Moebius Factor: Wall Rescue", SC2WOL_LOC_ID_OFFSET + 1004,
|
||||
@@ -131,9 +128,10 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
|
||||
LocationData("The Moebius Factor", "The Moebius Factor: Alive Inside Rescue", SC2WOL_LOC_ID_OFFSET + 1007,
|
||||
lambda state: state._sc2wol_able_to_rescue(world, player)),
|
||||
LocationData("The Moebius Factor", "The Moebius Factor: Brutalisk", SC2WOL_LOC_ID_OFFSET + 1008,
|
||||
lambda state: state._sc2wol_has_air(world, player)),
|
||||
LocationData("The Moebius Factor", "Beat The Moebius Factor", None,
|
||||
lambda state: state._sc2wol_has_air(world, player)),
|
||||
lambda state: state._sc2wol_has_anti_air(world, player) and
|
||||
(state._sc2wol_has_air(world, player)
|
||||
or state.has_any({'Medivac', 'Hercules'}, player)
|
||||
and state._sc2wol_has_common_unit(world, player))),
|
||||
LocationData("Supernova", "Supernova: Victory", SC2WOL_LOC_ID_OFFSET + 1100,
|
||||
lambda state: state._sc2wol_beats_protoss_deathball(world, player)),
|
||||
LocationData("Supernova", "Supernova: West Relic", SC2WOL_LOC_ID_OFFSET + 1101),
|
||||
@@ -142,47 +140,24 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
|
||||
lambda state: state._sc2wol_beats_protoss_deathball(world, player)),
|
||||
LocationData("Supernova", "Supernova: East Relic", SC2WOL_LOC_ID_OFFSET + 1104,
|
||||
lambda state: state._sc2wol_beats_protoss_deathball(world, player)),
|
||||
LocationData("Supernova", "Beat Supernova", None,
|
||||
lambda state: state._sc2wol_beats_protoss_deathball(world, player)),
|
||||
LocationData("Maw of the Void", "Maw of the Void: Victory", SC2WOL_LOC_ID_OFFSET + 1200,
|
||||
lambda state: state.has('Battlecruiser', player) or
|
||||
state._sc2wol_has_air(world, player) and
|
||||
state._sc2wol_has_competent_anti_air(world, player) and
|
||||
state.has('Science Vessel', player)),
|
||||
lambda state: state._sc2wol_survives_rip_field(world, player)),
|
||||
LocationData("Maw of the Void", "Maw of the Void: Landing Zone Cleared", SC2WOL_LOC_ID_OFFSET + 1201),
|
||||
LocationData("Maw of the Void", "Maw of the Void: Expansion Prisoners", SC2WOL_LOC_ID_OFFSET + 1202,
|
||||
lambda state: state.has('Battlecruiser', player) or
|
||||
state._sc2wol_has_air(world, player) and
|
||||
state._sc2wol_has_competent_anti_air(world, player) and
|
||||
state.has('Science Vessel', player)),
|
||||
lambda state: logic_level > 0 or state._sc2wol_survives_rip_field(world, player)),
|
||||
LocationData("Maw of the Void", "Maw of the Void: South Close Prisoners", SC2WOL_LOC_ID_OFFSET + 1203,
|
||||
lambda state: state.has('Battlecruiser', player) or
|
||||
state._sc2wol_has_air(world, player) and
|
||||
state._sc2wol_has_competent_anti_air(world, player) and
|
||||
state.has('Science Vessel', player)),
|
||||
lambda state: logic_level > 0 or state._sc2wol_survives_rip_field(world, player)),
|
||||
LocationData("Maw of the Void", "Maw of the Void: South Far Prisoners", SC2WOL_LOC_ID_OFFSET + 1204,
|
||||
lambda state: state.has('Battlecruiser', player) or
|
||||
state._sc2wol_has_air(world, player) and
|
||||
state._sc2wol_has_competent_anti_air(world, player) and
|
||||
state.has('Science Vessel', player)),
|
||||
lambda state: state._sc2wol_survives_rip_field(world, player)),
|
||||
LocationData("Maw of the Void", "Maw of the Void: North Prisoners", SC2WOL_LOC_ID_OFFSET + 1205,
|
||||
lambda state: state.has('Battlecruiser', player) or
|
||||
state._sc2wol_has_air(world, player) and
|
||||
state._sc2wol_has_competent_anti_air(world, player) and
|
||||
state.has('Science Vessel', player)),
|
||||
LocationData("Maw of the Void", "Beat Maw of the Void", None,
|
||||
lambda state: state.has('Battlecruiser', player) or
|
||||
state._sc2wol_has_air(world, player) and
|
||||
state._sc2wol_has_competent_anti_air(world, player) and
|
||||
state.has('Science Vessel', player)),
|
||||
lambda state: state._sc2wol_survives_rip_field(world, player)),
|
||||
LocationData("Devil's Playground", "Devil's Playground: Victory", SC2WOL_LOC_ID_OFFSET + 1300,
|
||||
lambda state: state._sc2wol_has_anti_air(world, player) and (
|
||||
state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player))),
|
||||
lambda state: logic_level > 0 or
|
||||
state._sc2wol_has_anti_air(world, player) and (
|
||||
state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player))),
|
||||
LocationData("Devil's Playground", "Devil's Playground: Tosh's Miners", SC2WOL_LOC_ID_OFFSET + 1301),
|
||||
LocationData("Devil's Playground", "Devil's Playground: Brutalisk", SC2WOL_LOC_ID_OFFSET + 1302,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)),
|
||||
LocationData("Devil's Playground", "Beat Devil's Playground", None,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)),
|
||||
lambda state: logic_level > 0 or state._sc2wol_has_common_unit(world, player) or state.has("Reaper", player)),
|
||||
LocationData("Welcome to the Jungle", "Welcome to the Jungle: Victory", SC2WOL_LOC_ID_OFFSET + 1400,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player) and
|
||||
state._sc2wol_has_competent_anti_air(world, player)),
|
||||
@@ -193,30 +168,24 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
|
||||
LocationData("Welcome to the Jungle", "Welcome to the Jungle: North-East Relic", SC2WOL_LOC_ID_OFFSET + 1403,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player) and
|
||||
state._sc2wol_has_competent_anti_air(world, player)),
|
||||
LocationData("Welcome to the Jungle", "Beat Welcome to the Jungle", None,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player) and
|
||||
state._sc2wol_has_competent_anti_air(world, player)),
|
||||
LocationData("Breakout", "Breakout: Victory", SC2WOL_LOC_ID_OFFSET + 1500),
|
||||
LocationData("Breakout", "Breakout: Diamondback Prison", SC2WOL_LOC_ID_OFFSET + 1501),
|
||||
LocationData("Breakout", "Breakout: Siegetank Prison", SC2WOL_LOC_ID_OFFSET + 1502),
|
||||
LocationData("Breakout", "Beat Breakout", None),
|
||||
LocationData("Ghost of a Chance", "Ghost of a Chance: Victory", SC2WOL_LOC_ID_OFFSET + 1600),
|
||||
LocationData("Ghost of a Chance", "Ghost of a Chance: Terrazine Tank", SC2WOL_LOC_ID_OFFSET + 1601),
|
||||
LocationData("Ghost of a Chance", "Ghost of a Chance: Jorium Stockpile", SC2WOL_LOC_ID_OFFSET + 1602),
|
||||
LocationData("Ghost of a Chance", "Ghost of a Chance: First Island Spectres", SC2WOL_LOC_ID_OFFSET + 1603),
|
||||
LocationData("Ghost of a Chance", "Ghost of a Chance: Second Island Spectres", SC2WOL_LOC_ID_OFFSET + 1604),
|
||||
LocationData("Ghost of a Chance", "Ghost of a Chance: Third Island Spectres", SC2WOL_LOC_ID_OFFSET + 1605),
|
||||
LocationData("Ghost of a Chance", "Beat Ghost of a Chance", None),
|
||||
LocationData("The Great Train Robbery", "The Great Train Robbery: Victory", SC2WOL_LOC_ID_OFFSET + 1700,
|
||||
lambda state: state._sc2wol_has_train_killers(world, player) and
|
||||
state._sc2wol_has_anti_air(world, player)),
|
||||
LocationData("The Great Train Robbery", "The Great Train Robbery: North Defiler", SC2WOL_LOC_ID_OFFSET + 1701),
|
||||
LocationData("The Great Train Robbery", "The Great Train Robbery: Mid Defiler", SC2WOL_LOC_ID_OFFSET + 1702),
|
||||
LocationData("The Great Train Robbery", "The Great Train Robbery: South Defiler", SC2WOL_LOC_ID_OFFSET + 1703),
|
||||
LocationData("The Great Train Robbery", "Beat The Great Train Robbery", None,
|
||||
lambda state: state._sc2wol_has_train_killers(world, player)),
|
||||
LocationData("Cutthroat", "Cutthroat: Victory", SC2WOL_LOC_ID_OFFSET + 1800,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player)),
|
||||
lambda state: state._sc2wol_has_common_unit(world, player) and
|
||||
(logic_level > 0 or state._sc2wol_has_anti_air)),
|
||||
LocationData("Cutthroat", "Cutthroat: Mira Han", SC2WOL_LOC_ID_OFFSET + 1801,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player)),
|
||||
LocationData("Cutthroat", "Cutthroat: North Relic", SC2WOL_LOC_ID_OFFSET + 1802,
|
||||
@@ -224,10 +193,9 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
|
||||
LocationData("Cutthroat", "Cutthroat: Mid Relic", SC2WOL_LOC_ID_OFFSET + 1803),
|
||||
LocationData("Cutthroat", "Cutthroat: Southwest Relic", SC2WOL_LOC_ID_OFFSET + 1804,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player)),
|
||||
LocationData("Cutthroat", "Beat Cutthroat", None,
|
||||
lambda state: state._sc2wol_has_common_unit(world, player)),
|
||||
LocationData("Engine of Destruction", "Engine of Destruction: Victory", SC2WOL_LOC_ID_OFFSET + 1900,
|
||||
lambda state: state._sc2wol_has_competent_anti_air(world, player)),
|
||||
lambda state: state._sc2wol_has_competent_anti_air(world, player) and
|
||||
state._sc2wol_has_common_unit(world, player) or state.has('Wraith', player)),
|
||||
LocationData("Engine of Destruction", "Engine of Destruction: Odin", SC2WOL_LOC_ID_OFFSET + 1901),
|
||||
LocationData("Engine of Destruction", "Engine of Destruction: Loki", SC2WOL_LOC_ID_OFFSET + 1902,
|
||||
lambda state: state._sc2wol_has_competent_anti_air(world, player) and
|
||||
@@ -239,9 +207,6 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
|
||||
LocationData("Engine of Destruction", "Engine of Destruction: Southeast Devourer", SC2WOL_LOC_ID_OFFSET + 1905,
|
||||
lambda state: state._sc2wol_has_competent_anti_air(world, player) and
|
||||
state._sc2wol_has_common_unit(world, player) or state.has('Wraith', player)),
|
||||
LocationData("Engine of Destruction", "Beat Engine of Destruction", None,
|
||||
lambda state: state._sc2wol_has_competent_anti_air(world, player) and
|
||||
state._sc2wol_has_common_unit(world, player) or state.has('Wraith', player)),
|
||||
LocationData("Media Blitz", "Media Blitz: Victory", SC2WOL_LOC_ID_OFFSET + 2000,
|
||||
lambda state: state._sc2wol_has_competent_comp(world, player)),
|
||||
LocationData("Media Blitz", "Media Blitz: Tower 1", SC2WOL_LOC_ID_OFFSET + 2001,
|
||||
@@ -251,57 +216,49 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
|
||||
LocationData("Media Blitz", "Media Blitz: Tower 3", SC2WOL_LOC_ID_OFFSET + 2003,
|
||||
lambda state: state._sc2wol_has_competent_comp(world, player)),
|
||||
LocationData("Media Blitz", "Media Blitz: Science Facility", SC2WOL_LOC_ID_OFFSET + 2004),
|
||||
LocationData("Media Blitz", "Beat Media Blitz", None,
|
||||
lambda state: state._sc2wol_has_competent_comp(world, player)),
|
||||
LocationData("Piercing the Shroud", "Piercing the Shroud: Victory", SC2WOL_LOC_ID_OFFSET + 2100,
|
||||
lambda state: state.has_any({'Combat Shield (Marine)', 'Stabilizer Medpacks (Medic)'}, player)),
|
||||
lambda state: state._sc2wol_has_mm_upgrade(world, player)),
|
||||
LocationData("Piercing the Shroud", "Piercing the Shroud: Holding Cell Relic", SC2WOL_LOC_ID_OFFSET + 2101),
|
||||
LocationData("Piercing the Shroud", "Piercing the Shroud: Brutalisk Relic", SC2WOL_LOC_ID_OFFSET + 2102,
|
||||
lambda state: state.has_any({'Combat Shield (Marine)', 'Stabilizer Medpacks (Medic)'}, player)),
|
||||
lambda state: state._sc2wol_has_mm_upgrade(world, player)),
|
||||
LocationData("Piercing the Shroud", "Piercing the Shroud: First Escape Relic", SC2WOL_LOC_ID_OFFSET + 2103,
|
||||
lambda state: state.has_any({'Combat Shield (Marine)', 'Stabilizer Medpacks (Medic)'}, player)),
|
||||
lambda state: state._sc2wol_has_mm_upgrade(world, player)),
|
||||
LocationData("Piercing the Shroud", "Piercing the Shroud: Second Escape Relic", SC2WOL_LOC_ID_OFFSET + 2104,
|
||||
lambda state: state.has_any({'Combat Shield (Marine)', 'Stabilizer Medpacks (Medic)'}, player)),
|
||||
lambda state: state._sc2wol_has_mm_upgrade(world, player)),
|
||||
LocationData("Piercing the Shroud", "Piercing the Shroud: Brutalisk ", SC2WOL_LOC_ID_OFFSET + 2105,
|
||||
lambda state: state.has_any({'Combat Shield (Marine)', 'Stabilizer Medpacks (Medic)'}, player)),
|
||||
LocationData("Piercing the Shroud", "Beat Piercing the Shroud", None,
|
||||
lambda state: state.has_any({'Combat Shield (Marine)', 'Stabilizer Medpacks (Medic)'}, player)),
|
||||
lambda state: state._sc2wol_has_mm_upgrade(world, player)),
|
||||
LocationData("Whispers of Doom", "Whispers of Doom: Victory", SC2WOL_LOC_ID_OFFSET + 2200),
|
||||
LocationData("Whispers of Doom", "Whispers of Doom: First Hatchery", SC2WOL_LOC_ID_OFFSET + 2201),
|
||||
LocationData("Whispers of Doom", "Whispers of Doom: Second Hatchery", SC2WOL_LOC_ID_OFFSET + 2202),
|
||||
LocationData("Whispers of Doom", "Whispers of Doom: Third Hatchery", SC2WOL_LOC_ID_OFFSET + 2203),
|
||||
LocationData("Whispers of Doom", "Beat Whispers of Doom", None),
|
||||
LocationData("A Sinister Turn", "A Sinister Turn: Victory", SC2WOL_LOC_ID_OFFSET + 2300,
|
||||
lambda state: state._sc2wol_has_protoss_medium_units(world, player)),
|
||||
LocationData("A Sinister Turn", "A Sinister Turn: Robotics Facility", SC2WOL_LOC_ID_OFFSET + 2301),
|
||||
LocationData("A Sinister Turn", "A Sinister Turn: Dark Shrine", SC2WOL_LOC_ID_OFFSET + 2302),
|
||||
LocationData("A Sinister Turn", "A Sinister Turn: Robotics Facility", SC2WOL_LOC_ID_OFFSET + 2301,
|
||||
lambda state: logic_level > 0 or state._sc2wol_has_protoss_common_units(world, player)),
|
||||
LocationData("A Sinister Turn", "A Sinister Turn: Dark Shrine", SC2WOL_LOC_ID_OFFSET + 2302,
|
||||
lambda state: logic_level > 0 or state._sc2wol_has_protoss_common_units(world, player)),
|
||||
LocationData("A Sinister Turn", "A Sinister Turn: Templar Archives", SC2WOL_LOC_ID_OFFSET + 2303,
|
||||
lambda state: state._sc2wol_has_protoss_common_units(world, player)),
|
||||
LocationData("A Sinister Turn", "Beat A Sinister Turn", None,
|
||||
lambda state: state._sc2wol_has_protoss_medium_units(world, player)),
|
||||
LocationData("Echoes of the Future", "Echoes of the Future: Victory", SC2WOL_LOC_ID_OFFSET + 2400,
|
||||
lambda state: state._sc2wol_has_protoss_medium_units(world, player)),
|
||||
lambda state: logic_level > 0 or state._sc2wol_has_protoss_medium_units(world, player)),
|
||||
LocationData("Echoes of the Future", "Echoes of the Future: Close Obelisk", SC2WOL_LOC_ID_OFFSET + 2401),
|
||||
LocationData("Echoes of the Future", "Echoes of the Future: West Obelisk", SC2WOL_LOC_ID_OFFSET + 2402,
|
||||
lambda state: state._sc2wol_has_protoss_common_units(world, player)),
|
||||
LocationData("Echoes of the Future", "Beat Echoes of the Future", None,
|
||||
lambda state: state._sc2wol_has_protoss_medium_units(world, player)),
|
||||
lambda state: logic_level > 0 or state._sc2wol_has_protoss_common_units(world, player)),
|
||||
LocationData("In Utter Darkness", "In Utter Darkness: Defeat", SC2WOL_LOC_ID_OFFSET + 2500),
|
||||
LocationData("In Utter Darkness", "In Utter Darkness: Protoss Archive", SC2WOL_LOC_ID_OFFSET + 2501,
|
||||
lambda state: state._sc2wol_has_protoss_medium_units(world, player)),
|
||||
LocationData("In Utter Darkness", "In Utter Darkness: Kills", SC2WOL_LOC_ID_OFFSET + 2502,
|
||||
lambda state: state._sc2wol_has_protoss_common_units(world, player)),
|
||||
LocationData("In Utter Darkness", "Beat In Utter Darkness", None),
|
||||
LocationData("Gates of Hell", "Gates of Hell: Victory", SC2WOL_LOC_ID_OFFSET + 2600,
|
||||
lambda state: state._sc2wol_has_competent_comp(world, player)),
|
||||
lambda state: state._sc2wol_has_competent_comp(world, player) and
|
||||
state._sc2wol_defense_rating(world, player, True) > 6),
|
||||
LocationData("Gates of Hell", "Gates of Hell: Large Army", SC2WOL_LOC_ID_OFFSET + 2601,
|
||||
lambda state: state._sc2wol_has_competent_comp(world, player)),
|
||||
LocationData("Gates of Hell", "Beat Gates of Hell", None),
|
||||
lambda state: state._sc2wol_has_competent_comp(world, player) and
|
||||
state._sc2wol_defense_rating(world, player, True) > 6),
|
||||
LocationData("Belly of the Beast", "Belly of the Beast: Victory", SC2WOL_LOC_ID_OFFSET + 2700),
|
||||
LocationData("Belly of the Beast", "Belly of the Beast: First Charge", SC2WOL_LOC_ID_OFFSET + 2701),
|
||||
LocationData("Belly of the Beast", "Belly of the Beast: Second Charge", SC2WOL_LOC_ID_OFFSET + 2702),
|
||||
LocationData("Belly of the Beast", "Belly of the Beast: Third Charge", SC2WOL_LOC_ID_OFFSET + 2703),
|
||||
LocationData("Belly of the Beast", "Beat Belly of the Beast", None),
|
||||
LocationData("Shatter the Sky", "Shatter the Sky: Victory", SC2WOL_LOC_ID_OFFSET + 2800,
|
||||
lambda state: state._sc2wol_has_competent_comp(world, player)),
|
||||
LocationData("Shatter the Sky", "Shatter the Sky: Close Coolant Tower", SC2WOL_LOC_ID_OFFSET + 2801,
|
||||
@@ -314,9 +271,19 @@ def get_locations(world: Optional[MultiWorld], player: Optional[int]) -> Tuple[L
|
||||
lambda state: state._sc2wol_has_competent_comp(world, player)),
|
||||
LocationData("Shatter the Sky", "Shatter the Sky: Leviathan", SC2WOL_LOC_ID_OFFSET + 2805,
|
||||
lambda state: state._sc2wol_has_competent_comp(world, player)),
|
||||
LocationData("Shatter the Sky", "Beat Shatter the Sky", None,
|
||||
lambda state: state._sc2wol_has_competent_comp(world, player)),
|
||||
LocationData("All-In", "All-In: Victory", None)
|
||||
LocationData("All-In", "All-In: Victory", None,
|
||||
lambda state: state._sc2wol_final_mission_requirements(world, player))
|
||||
]
|
||||
|
||||
return tuple(location_table)
|
||||
beat_events = []
|
||||
|
||||
for i, location_data in enumerate(location_table):
|
||||
# Removing all item-based logic on No Logic
|
||||
if logic_level == 2:
|
||||
location_table[i] = location_data._replace(rule=Location.access_rule)
|
||||
# Generating Beat event locations
|
||||
if location_data.name.endswith((": Victory", ": Defeat")):
|
||||
beat_events.append(
|
||||
location_data._replace(name="Beat " + location_data.name.rsplit(": ", 1)[0], code=None)
|
||||
)
|
||||
return tuple(location_table + beat_events)
|
||||
|
||||
+58
-21
@@ -1,31 +1,43 @@
|
||||
from BaseClasses import MultiWorld
|
||||
from ..AutoWorld import LogicMixin
|
||||
from worlds.AutoWorld import LogicMixin
|
||||
from .Options import get_option_value
|
||||
from .Items import get_basic_units, defense_ratings, zerg_defense_ratings
|
||||
|
||||
|
||||
class SC2WoLLogic(LogicMixin):
|
||||
def _sc2wol_has_common_unit(self, world: MultiWorld, player: int) -> bool:
|
||||
return self.has_any({'Marine', 'Marauder', 'Firebat', 'Hellion', 'Vulture'}, player)
|
||||
|
||||
def _sc2wol_has_bunker_unit(self, world: MultiWorld, player: int) -> bool:
|
||||
return self.has_any({'Marine', 'Marauder'}, player)
|
||||
return self.has_any(get_basic_units(world, player), player)
|
||||
|
||||
def _sc2wol_has_air(self, world: MultiWorld, player: int) -> bool:
|
||||
return self.has_any({'Viking', 'Wraith', 'Banshee'}, player) or \
|
||||
self.has_any({'Hercules', 'Medivac'}, player) and self._sc2wol_has_common_unit(world, player)
|
||||
return self.has_any({'Viking', 'Wraith', 'Banshee'}, player) or get_option_value(world, player, 'required_tactics') > 0 \
|
||||
and self.has_any({'Hercules', 'Medivac'}, player) and self._sc2wol_has_common_unit(world, player)
|
||||
|
||||
def _sc2wol_has_air_anti_air(self, world: MultiWorld, player: int) -> bool:
|
||||
return self.has_any({'Viking', 'Wraith'}, player)
|
||||
return self.has('Viking', player) \
|
||||
or get_option_value(world, player, 'required_tactics') > 0 and self.has('Wraith', player)
|
||||
|
||||
def _sc2wol_has_competent_anti_air(self, world: MultiWorld, player: int) -> bool:
|
||||
return self.has_any({'Marine', 'Goliath'}, player) or self._sc2wol_has_air_anti_air(world, player)
|
||||
|
||||
def _sc2wol_has_anti_air(self, world: MultiWorld, player: int) -> bool:
|
||||
return self.has_any({'Missile Turret', 'Thor', 'War Pigs', 'Spartan Company', "Hel's Angel", 'Battlecruiser'}, player) or self._sc2wol_has_competent_anti_air(world, player)
|
||||
return self.has_any({'Missile Turret', 'Thor', 'War Pigs', 'Spartan Company', "Hel's Angel", 'Battlecruiser', 'Wraith'}, player) \
|
||||
or self._sc2wol_has_competent_anti_air(world, player) \
|
||||
or get_option_value(world, player, 'required_tactics') > 0 and self.has_any({'Ghost', 'Spectre'}, player)
|
||||
|
||||
def _sc2wol_has_heavy_defense(self, world: MultiWorld, player: int) -> bool:
|
||||
return (self.has_any({'Siege Tank', 'Vulture'}, player) or
|
||||
self.has('Bunker', player) and self._sc2wol_has_bunker_unit(world, player)) and \
|
||||
self._sc2wol_has_anti_air(world, player)
|
||||
def _sc2wol_defense_rating(self, world: MultiWorld, player: int, zerg_enemy: bool, air_enemy: bool = True) -> bool:
|
||||
defense_score = sum((defense_ratings[item] for item in defense_ratings if self.has(item, player)))
|
||||
if self.has_any({'Marine', 'Marauder'}, player) and self.has('Bunker', player):
|
||||
defense_score += 3
|
||||
if zerg_enemy:
|
||||
defense_score += sum((zerg_defense_ratings[item] for item in zerg_defense_ratings if self.has(item, player)))
|
||||
if self.has('Firebat', player) and self.has('Bunker', player):
|
||||
defense_score += 2
|
||||
if not air_enemy and self.has('Missile Turret', player):
|
||||
defense_score -= defense_ratings['Missile Turret']
|
||||
# Advanced Tactics bumps defense rating requirements down by 2
|
||||
if get_option_value(world, player, 'required_tactics') > 0:
|
||||
defense_score += 2
|
||||
return defense_score
|
||||
|
||||
def _sc2wol_has_competent_comp(self, world: MultiWorld, player: int) -> bool:
|
||||
return (self.has('Marine', player) or self.has('Marauder', player) and
|
||||
@@ -35,25 +47,50 @@ class SC2WoLLogic(LogicMixin):
|
||||
self.has('Siege Tank', player) and self._sc2wol_has_competent_anti_air(world, player)
|
||||
|
||||
def _sc2wol_has_train_killers(self, world: MultiWorld, player: int) -> bool:
|
||||
return (self.has_any({'Siege Tank', 'Diamondback'}, player) or
|
||||
self.has_all({'Reaper', "G-4 Clusterbomb"}, player) or self.has_all({'Spectre', 'Psionic Lash'}, player)
|
||||
or self.has('Marauders', player))
|
||||
return (self.has_any({'Siege Tank', 'Diamondback', 'Marauder'}, player) or get_option_value(world, player, 'required_tactics') > 0
|
||||
and self.has_all({'Reaper', "G-4 Clusterbomb"}, player) or self.has_all({'Spectre', 'Psionic Lash'}, player))
|
||||
|
||||
def _sc2wol_able_to_rescue(self, world: MultiWorld, player: int) -> bool:
|
||||
return self.has_any({'Medivac', 'Hercules', 'Raven', 'Viking'}, player)
|
||||
return self.has_any({'Medivac', 'Hercules', 'Raven', 'Viking'}, player) or get_option_value(world, player, 'required_tactics') > 0
|
||||
|
||||
def _sc2wol_has_protoss_common_units(self, world: MultiWorld, player: int) -> bool:
|
||||
return self.has_any({'Zealot', 'Immortal', 'Stalker', 'Dark Templar'}, player)
|
||||
return self.has_any({'Zealot', 'Immortal', 'Stalker', 'Dark Templar'}, player) \
|
||||
or get_option_value(world, player, 'required_tactics') > 0 and self.has_any({'High Templar', 'Dark Templar'}, player)
|
||||
|
||||
def _sc2wol_has_protoss_medium_units(self, world: MultiWorld, player: int) -> bool:
|
||||
return self._sc2wol_has_protoss_common_units(world, player) and \
|
||||
self.has_any({'Stalker', 'Void Ray', 'Phoenix', 'Carrier'}, player)
|
||||
self.has_any({'Stalker', 'Void Ray', 'Phoenix', 'Carrier'}, player) \
|
||||
or get_option_value(world, player, 'required_tactics') > 0 and self.has_any({'High Templar', 'Dark Templar'}, player)
|
||||
|
||||
def _sc2wol_beats_protoss_deathball(self, world: MultiWorld, player: int) -> bool:
|
||||
return self.has_any({'Banshee', 'Battlecruiser'}, player) and self._sc2wol_has_competent_anti_air or \
|
||||
self._sc2wol_has_competent_comp(world, player) and self._sc2wol_has_air_anti_air(world, player)
|
||||
|
||||
def _sc2wol_has_mm_upgrade(self, world: MultiWorld, player: int) -> bool:
|
||||
return self.has_any({"Combat Shield (Marine)", "Stabilizer Medpacks (Medic)"}, player)
|
||||
|
||||
def _sc2wol_survives_rip_field(self, world: MultiWorld, player: int) -> bool:
|
||||
return self.has("Battlecruiser", player) or \
|
||||
self._sc2wol_has_air(world, player) and \
|
||||
self._sc2wol_has_competent_anti_air(world, player) and \
|
||||
self.has("Science Vessel", player)
|
||||
|
||||
def _sc2wol_has_nukes(self, world: MultiWorld, player: int) -> bool:
|
||||
return get_option_value(world, player, 'required_tactics') > 0 and self.has_any({'Ghost', 'Spectre'}, player)
|
||||
|
||||
def _sc2wol_final_mission_requirements(self, world: MultiWorld, player: int):
|
||||
defense_rating = self._sc2wol_defense_rating(world, player, True)
|
||||
beats_kerrigan = self.has_any({'Marine', 'Banshee', 'Ghost'}, player) or get_option_value(world, player, 'required_tactics') > 0
|
||||
if get_option_value(world, player, 'all_in_map') == 0:
|
||||
# Ground
|
||||
if self.has_any({'Battlecruiser', 'Banshee'}, player):
|
||||
defense_rating += 3
|
||||
return defense_rating >= 12 and beats_kerrigan
|
||||
else:
|
||||
# Air
|
||||
return defense_rating >= 8 and beats_kerrigan \
|
||||
and self.has_any({'Viking', 'Battlecruiser'}, player) \
|
||||
and self.has_any({'Hive Mind Emulator', 'Psi Disruptor', 'Missile Turret'}, player)
|
||||
|
||||
def _sc2wol_cleared_missions(self, world: MultiWorld, player: int, mission_count: int) -> bool:
|
||||
return self.has_group("Missions", player, mission_count)
|
||||
|
||||
|
||||
|
||||
+151
-31
@@ -1,4 +1,7 @@
|
||||
from typing import NamedTuple, Dict, List
|
||||
from typing import NamedTuple, Dict, List, Set
|
||||
|
||||
from BaseClasses import MultiWorld
|
||||
from .Options import get_option_value
|
||||
|
||||
no_build_regions_list = ["Liberation Day", "Breakout", "Ghost of a Chance", "Piercing the Shroud", "Whispers of Doom",
|
||||
"Belly of the Beast"]
|
||||
@@ -12,7 +15,6 @@ hard_regions_list = ["Maw of the Void", "Engine of Destruction", "In Utter Darkn
|
||||
|
||||
class MissionInfo(NamedTuple):
|
||||
id: int
|
||||
extra_locations: int
|
||||
required_world: List[int]
|
||||
category: str
|
||||
number: int = 0 # number of worlds need beaten
|
||||
@@ -62,38 +64,156 @@ vanilla_shuffle_order = [
|
||||
FillMission("all_in", [26, 27], "Char", completion_critical=True, or_requirements=True)
|
||||
]
|
||||
|
||||
mini_campaign_order = [
|
||||
FillMission("no_build", [-1], "Mar Sara", completion_critical=True),
|
||||
FillMission("easy", [0], "Colonist"),
|
||||
FillMission("medium", [1], "Colonist"),
|
||||
FillMission("medium", [0], "Artifact", completion_critical=True),
|
||||
FillMission("medium", [3], "Artifact", number=4, completion_critical=True),
|
||||
FillMission("hard", [4], "Artifact", number=8, completion_critical=True),
|
||||
FillMission("medium", [0], "Covert", number=2),
|
||||
FillMission("hard", [6], "Covert"),
|
||||
FillMission("medium", [0], "Rebellion", number=3),
|
||||
FillMission("hard", [8], "Rebellion"),
|
||||
FillMission("medium", [4], "Prophecy"),
|
||||
FillMission("hard", [10], "Prophecy"),
|
||||
FillMission("hard", [5], "Char", completion_critical=True),
|
||||
FillMission("hard", [5], "Char", completion_critical=True),
|
||||
FillMission("all_in", [12, 13], "Char", completion_critical=True, or_requirements=True)
|
||||
]
|
||||
|
||||
gauntlet_order = [
|
||||
FillMission("no_build", [-1], "I", completion_critical=True),
|
||||
FillMission("easy", [0], "II", completion_critical=True),
|
||||
FillMission("medium", [1], "III", completion_critical=True),
|
||||
FillMission("medium", [2], "IV", completion_critical=True),
|
||||
FillMission("hard", [3], "V", completion_critical=True),
|
||||
FillMission("hard", [4], "VI", completion_critical=True),
|
||||
FillMission("all_in", [5], "Final", completion_critical=True)
|
||||
]
|
||||
|
||||
grid_order = [
|
||||
FillMission("no_build", [-1], "_1"),
|
||||
FillMission("medium", [0], "_1"),
|
||||
FillMission("medium", [1, 6, 3], "_1", or_requirements=True),
|
||||
FillMission("hard", [2, 7], "_1", or_requirements=True),
|
||||
FillMission("easy", [0], "_2"),
|
||||
FillMission("medium", [1, 4], "_2", or_requirements=True),
|
||||
FillMission("hard", [2, 5, 10, 7], "_2", or_requirements=True),
|
||||
FillMission("hard", [3, 6, 11], "_2", or_requirements=True),
|
||||
FillMission("medium", [4, 9, 12], "_3", or_requirements=True),
|
||||
FillMission("hard", [5, 8, 10, 13], "_3", or_requirements=True),
|
||||
FillMission("hard", [6, 9, 11, 14], "_3", or_requirements=True),
|
||||
FillMission("hard", [7, 10], "_3", or_requirements=True),
|
||||
FillMission("hard", [8, 13], "_4", or_requirements=True),
|
||||
FillMission("hard", [9, 12, 14], "_4", or_requirements=True),
|
||||
FillMission("hard", [10, 13], "_4", or_requirements=True),
|
||||
FillMission("all_in", [11, 14], "_4", or_requirements=True)
|
||||
]
|
||||
|
||||
mini_grid_order = [
|
||||
FillMission("no_build", [-1], "_1"),
|
||||
FillMission("medium", [0], "_1"),
|
||||
FillMission("medium", [1, 5], "_1", or_requirements=True),
|
||||
FillMission("easy", [0], "_2"),
|
||||
FillMission("medium", [1, 3], "_2", or_requirements=True),
|
||||
FillMission("hard", [2, 4], "_2", or_requirements=True),
|
||||
FillMission("medium", [3, 7], "_3", or_requirements=True),
|
||||
FillMission("hard", [4, 6], "_3", or_requirements=True),
|
||||
FillMission("all_in", [5, 7], "_3", or_requirements=True)
|
||||
]
|
||||
|
||||
blitz_order = [
|
||||
FillMission("no_build", [-1], "I"),
|
||||
FillMission("easy", [-1], "I"),
|
||||
FillMission("medium", [0, 1], "II", number=1, or_requirements=True),
|
||||
FillMission("medium", [0, 1], "II", number=1, or_requirements=True),
|
||||
FillMission("medium", [0, 1], "III", number=2, or_requirements=True),
|
||||
FillMission("medium", [0, 1], "III", number=2, or_requirements=True),
|
||||
FillMission("hard", [0, 1], "IV", number=3, or_requirements=True),
|
||||
FillMission("hard", [0, 1], "IV", number=3, or_requirements=True),
|
||||
FillMission("hard", [0, 1], "V", number=4, or_requirements=True),
|
||||
FillMission("hard", [0, 1], "V", number=4, or_requirements=True),
|
||||
FillMission("hard", [0, 1], "Final", number=5, or_requirements=True),
|
||||
FillMission("all_in", [0, 1], "Final", number=5, or_requirements=True)
|
||||
]
|
||||
|
||||
mission_orders = [vanilla_shuffle_order, vanilla_shuffle_order, mini_campaign_order, grid_order, mini_grid_order, blitz_order, gauntlet_order]
|
||||
|
||||
|
||||
vanilla_mission_req_table = {
|
||||
"Liberation Day": MissionInfo(1, 7, [], "Mar Sara", completion_critical=True),
|
||||
"The Outlaws": MissionInfo(2, 2, [1], "Mar Sara", completion_critical=True),
|
||||
"Zero Hour": MissionInfo(3, 4, [2], "Mar Sara", completion_critical=True),
|
||||
"Evacuation": MissionInfo(4, 4, [3], "Colonist"),
|
||||
"Outbreak": MissionInfo(5, 3, [4], "Colonist"),
|
||||
"Safe Haven": MissionInfo(6, 4, [5], "Colonist", number=7),
|
||||
"Haven's Fall": MissionInfo(7, 4, [5], "Colonist", number=7),
|
||||
"Smash and Grab": MissionInfo(8, 5, [3], "Artifact", completion_critical=True),
|
||||
"The Dig": MissionInfo(9, 4, [8], "Artifact", number=8, completion_critical=True),
|
||||
"The Moebius Factor": MissionInfo(10, 9, [9], "Artifact", number=11, completion_critical=True),
|
||||
"Supernova": MissionInfo(11, 5, [10], "Artifact", number=14, completion_critical=True),
|
||||
"Maw of the Void": MissionInfo(12, 6, [11], "Artifact", completion_critical=True),
|
||||
"Devil's Playground": MissionInfo(13, 3, [3], "Covert", number=4),
|
||||
"Welcome to the Jungle": MissionInfo(14, 4, [13], "Covert"),
|
||||
"Breakout": MissionInfo(15, 3, [14], "Covert", number=8),
|
||||
"Ghost of a Chance": MissionInfo(16, 6, [14], "Covert", number=8),
|
||||
"The Great Train Robbery": MissionInfo(17, 4, [3], "Rebellion", number=6),
|
||||
"Cutthroat": MissionInfo(18, 5, [17], "Rebellion"),
|
||||
"Engine of Destruction": MissionInfo(19, 6, [18], "Rebellion"),
|
||||
"Media Blitz": MissionInfo(20, 5, [19], "Rebellion"),
|
||||
"Piercing the Shroud": MissionInfo(21, 6, [20], "Rebellion"),
|
||||
"Whispers of Doom": MissionInfo(22, 4, [9], "Prophecy"),
|
||||
"A Sinister Turn": MissionInfo(23, 4, [22], "Prophecy"),
|
||||
"Echoes of the Future": MissionInfo(24, 3, [23], "Prophecy"),
|
||||
"In Utter Darkness": MissionInfo(25, 3, [24], "Prophecy"),
|
||||
"Gates of Hell": MissionInfo(26, 2, [12], "Char", completion_critical=True),
|
||||
"Belly of the Beast": MissionInfo(27, 4, [26], "Char", completion_critical=True),
|
||||
"Shatter the Sky": MissionInfo(28, 5, [26], "Char", completion_critical=True),
|
||||
"All-In": MissionInfo(29, -1, [27, 28], "Char", completion_critical=True, or_requirements=True)
|
||||
"Liberation Day": MissionInfo(1, [], "Mar Sara", completion_critical=True),
|
||||
"The Outlaws": MissionInfo(2, [1], "Mar Sara", completion_critical=True),
|
||||
"Zero Hour": MissionInfo(3, [2], "Mar Sara", completion_critical=True),
|
||||
"Evacuation": MissionInfo(4, [3], "Colonist"),
|
||||
"Outbreak": MissionInfo(5, [4], "Colonist"),
|
||||
"Safe Haven": MissionInfo(6, [5], "Colonist", number=7),
|
||||
"Haven's Fall": MissionInfo(7, [5], "Colonist", number=7),
|
||||
"Smash and Grab": MissionInfo(8, [3], "Artifact", completion_critical=True),
|
||||
"The Dig": MissionInfo(9, [8], "Artifact", number=8, completion_critical=True),
|
||||
"The Moebius Factor": MissionInfo(10, [9], "Artifact", number=11, completion_critical=True),
|
||||
"Supernova": MissionInfo(11, [10], "Artifact", number=14, completion_critical=True),
|
||||
"Maw of the Void": MissionInfo(12, [11], "Artifact", completion_critical=True),
|
||||
"Devil's Playground": MissionInfo(13, [3], "Covert", number=4),
|
||||
"Welcome to the Jungle": MissionInfo(14, [13], "Covert"),
|
||||
"Breakout": MissionInfo(15, [14], "Covert", number=8),
|
||||
"Ghost of a Chance": MissionInfo(16, [14], "Covert", number=8),
|
||||
"The Great Train Robbery": MissionInfo(17, [3], "Rebellion", number=6),
|
||||
"Cutthroat": MissionInfo(18, [17], "Rebellion"),
|
||||
"Engine of Destruction": MissionInfo(19, [18], "Rebellion"),
|
||||
"Media Blitz": MissionInfo(20, [19], "Rebellion"),
|
||||
"Piercing the Shroud": MissionInfo(21, [20], "Rebellion"),
|
||||
"Whispers of Doom": MissionInfo(22, [9], "Prophecy"),
|
||||
"A Sinister Turn": MissionInfo(23, [22], "Prophecy"),
|
||||
"Echoes of the Future": MissionInfo(24, [23], "Prophecy"),
|
||||
"In Utter Darkness": MissionInfo(25, [24], "Prophecy"),
|
||||
"Gates of Hell": MissionInfo(26, [12], "Char", completion_critical=True),
|
||||
"Belly of the Beast": MissionInfo(27, [26], "Char", completion_critical=True),
|
||||
"Shatter the Sky": MissionInfo(28, [26], "Char", completion_critical=True),
|
||||
"All-In": MissionInfo(29, [27, 28], "Char", completion_critical=True, or_requirements=True)
|
||||
}
|
||||
|
||||
lookup_id_to_mission: Dict[int, str] = {
|
||||
data.id: mission_name for mission_name, data in vanilla_mission_req_table.items() if data.id}
|
||||
|
||||
no_build_starting_mission_locations = {
|
||||
"Liberation Day": "Liberation Day: Victory",
|
||||
"Breakout": "Breakout: Victory",
|
||||
"Ghost of a Chance": "Ghost of a Chance: Victory",
|
||||
"Piercing the Shroud": "Piercing the Shroud: Victory",
|
||||
"Whispers of Doom": "Whispers of Doom: Victory",
|
||||
"Belly of the Beast": "Belly of the Beast: Victory",
|
||||
}
|
||||
|
||||
build_starting_mission_locations = {
|
||||
"Zero Hour": "Zero Hour: First Group Rescued",
|
||||
"Evacuation": "Evacuation: First Chysalis",
|
||||
"Devil's Playground": "Devil's Playground: Tosh's Miners"
|
||||
}
|
||||
|
||||
advanced_starting_mission_locations = {
|
||||
"Smash and Grab": "Smash and Grab: First Relic",
|
||||
"The Great Train Robbery": "The Great Train Robbery: North Defiler"
|
||||
}
|
||||
|
||||
|
||||
def get_starting_mission_locations(world: MultiWorld, player: int) -> Set[str]:
|
||||
if get_option_value(world, player, 'shuffle_no_build') or get_option_value(world, player, 'mission_order') < 2:
|
||||
# Always start with a no-build mission unless explicitly relegating them
|
||||
# Vanilla and Vanilla Shuffled always start with a no-build even when relegated
|
||||
return no_build_starting_mission_locations
|
||||
elif get_option_value(world, player, 'required_tactics') > 0:
|
||||
# Advanced Tactics/No Logic add more starting missions to the pool
|
||||
return {**build_starting_mission_locations, **advanced_starting_mission_locations}
|
||||
else:
|
||||
# Standard starting missions when relegate is on
|
||||
return build_starting_mission_locations
|
||||
|
||||
|
||||
alt_final_mission_locations = {
|
||||
"Maw of the Void": "Maw of the Void: Victory",
|
||||
"Engine of Destruction": "Engine of Destruction: Victory",
|
||||
"Supernova": "Supernova: Victory",
|
||||
"Gates of Hell": "Gates of Hell: Victory",
|
||||
"Shatter the Sky": "Shatter the Sky: Victory"
|
||||
}
|
||||
+78
-13
@@ -1,6 +1,6 @@
|
||||
from typing import Dict
|
||||
from BaseClasses import MultiWorld
|
||||
from Options import Choice, Option, DefaultOnToggle
|
||||
from Options import Choice, Option, Toggle, DefaultOnToggle, ItemSet, OptionSet, Range
|
||||
|
||||
|
||||
class GameDifficulty(Choice):
|
||||
@@ -36,25 +36,75 @@ class AllInMap(Choice):
|
||||
|
||||
|
||||
class MissionOrder(Choice):
|
||||
"""Determines the order the missions are played in.
|
||||
Vanilla: Keeps the standard mission order and branching from the WoL Campaign.
|
||||
Vanilla Shuffled: Keeps same branching paths from the WoL Campaign but randomizes the order of missions within."""
|
||||
"""Determines the order the missions are played in. The last three mission orders end in a random mission.
|
||||
Vanilla (29): Keeps the standard mission order and branching from the WoL Campaign.
|
||||
Vanilla Shuffled (29): Keeps same branching paths from the WoL Campaign but randomizes the order of missions within.
|
||||
Mini Campaign (15): Shorter version of the campaign with randomized missions and optional branches.
|
||||
Grid (16): A 4x4 grid of random missions. Start at the top-left and forge a path towards All-In.
|
||||
Mini Grid (9): A 3x3 version of Grid. Complete the bottom-right mission to win.
|
||||
Blitz (12): 12 random missions that open up very quickly. Complete the bottom-right mission to win.
|
||||
Gauntlet (7): Linear series of 7 random missions to complete the campaign."""
|
||||
display_name = "Mission Order"
|
||||
option_vanilla = 0
|
||||
option_vanilla_shuffled = 1
|
||||
option_mini_campaign = 2
|
||||
option_grid = 3
|
||||
option_mini_grid = 4
|
||||
option_blitz = 5
|
||||
option_gauntlet = 6
|
||||
|
||||
|
||||
class ShuffleProtoss(DefaultOnToggle):
|
||||
"""Determines if the 3 protoss missions are included in the shuffle if Vanilla Shuffled is enabled. If this is
|
||||
not the 3 protoss missions will stay in their vanilla order in the mission order making them optional to complete
|
||||
the game."""
|
||||
"""Determines if the 3 protoss missions are included in the shuffle if Vanilla mission order is not enabled.
|
||||
If turned off with Vanilla Shuffled, the 3 protoss missions will be in their normal position on the Prophecy chain if not shuffled.
|
||||
If turned off with reduced mission settings, the 3 protoss missions will not appear and Protoss units are removed from the pool."""
|
||||
display_name = "Shuffle Protoss Missions"
|
||||
|
||||
|
||||
class RelegateNoBuildMissions(DefaultOnToggle):
|
||||
"""If enabled, all no build missions besides the needed first one will be placed at the end of optional routes so
|
||||
that none of them become required to complete the game. Only takes effect if mission order is not set to vanilla."""
|
||||
display_name = "Relegate No-Build Missions"
|
||||
class ShuffleNoBuild(DefaultOnToggle):
|
||||
"""Determines if the 5 no-build missions are included in the shuffle if Vanilla mission order is not enabled.
|
||||
If turned off with Vanilla Shuffled, one no-build mission will be placed as the first mission and the rest will be placed at the end of optional routes.
|
||||
If turned off with reduced mission settings, the 5 no-build missions will not appear."""
|
||||
display_name = "Shuffle No-Build Missions"
|
||||
|
||||
|
||||
class EarlyUnit(DefaultOnToggle):
|
||||
"""Guarantees that the first mission will contain a unit."""
|
||||
display_name = "Early Unit"
|
||||
|
||||
|
||||
class RequiredTactics(Choice):
|
||||
"""Determines the maximum tactical difficulty of the seed (separate from mission difficulty). Higher settings increase randomness.
|
||||
Standard: All missions can be completed with good micro and macro.
|
||||
Advanced: Completing missions may require relying on starting units and micro-heavy units.
|
||||
No Logic: Units and upgrades may be placed anywhere. LIKELY TO RENDER THE RUN IMPOSSIBLE ON HARDER DIFFICULTIES!"""
|
||||
display_name = "Required Tactics"
|
||||
option_standard = 0
|
||||
option_advanced = 1
|
||||
option_no_logic = 2
|
||||
|
||||
|
||||
class UnitsAlwaysHaveUpgrades(DefaultOnToggle):
|
||||
"""If turned on, both upgrades will be present for each unit and structure in the seed.
|
||||
This usually results in fewer units."""
|
||||
display_name = "Units Always Have Upgrades"
|
||||
|
||||
|
||||
class LockedItems(ItemSet):
|
||||
"""Guarantees that these items will be unlockable"""
|
||||
display_name = "Locked Items"
|
||||
|
||||
|
||||
class ExcludedItems(ItemSet):
|
||||
"""Guarantees that these items will not be unlockable"""
|
||||
display_name = "Excluded Items"
|
||||
|
||||
|
||||
class ExcludedMissions(OptionSet):
|
||||
"""Guarantees that these missions will not appear in the campaign
|
||||
Only applies on shortened mission orders.
|
||||
It may be impossible to build a valid campaign if too many missions are excluded."""
|
||||
display_name = "Excluded Missions"
|
||||
|
||||
|
||||
# noinspection PyTypeChecker
|
||||
@@ -65,14 +115,29 @@ sc2wol_options: Dict[str, Option] = {
|
||||
"all_in_map": AllInMap,
|
||||
"mission_order": MissionOrder,
|
||||
"shuffle_protoss": ShuffleProtoss,
|
||||
"relegate_no_build": RelegateNoBuildMissions
|
||||
"shuffle_no_build": ShuffleNoBuild,
|
||||
"early_unit": EarlyUnit,
|
||||
"required_tactics": RequiredTactics,
|
||||
"units_always_have_upgrades": UnitsAlwaysHaveUpgrades,
|
||||
"locked_items": LockedItems,
|
||||
"excluded_items": ExcludedItems,
|
||||
"excluded_missions": ExcludedMissions
|
||||
}
|
||||
|
||||
|
||||
def get_option_value(world: MultiWorld, player: int, name: str) -> int:
|
||||
option = getattr(world, name, None)
|
||||
|
||||
if option == None:
|
||||
if option is None:
|
||||
return 0
|
||||
|
||||
return int(option[player].value)
|
||||
|
||||
|
||||
def get_option_set_value(world: MultiWorld, player: int, name: str) -> set:
|
||||
option = getattr(world, name, None)
|
||||
|
||||
if option is None:
|
||||
return set()
|
||||
|
||||
return option[player].value
|
||||
|
||||
@@ -0,0 +1,257 @@
|
||||
from typing import Callable, Dict, List, Set
|
||||
from BaseClasses import MultiWorld, ItemClassification, Item, Location
|
||||
from .Items import item_table
|
||||
from .MissionTables import no_build_regions_list, easy_regions_list, medium_regions_list, hard_regions_list,\
|
||||
mission_orders, get_starting_mission_locations, MissionInfo, vanilla_mission_req_table, alt_final_mission_locations
|
||||
from .Options import get_option_value, get_option_set_value
|
||||
from .LogicMixin import SC2WoLLogic
|
||||
|
||||
# Items with associated upgrades
|
||||
UPGRADABLE_ITEMS = [
|
||||
"Marine", "Medic", "Firebat", "Marauder", "Reaper", "Ghost", "Spectre",
|
||||
"Hellion", "Vulture", "Goliath", "Diamondback", "Siege Tank", "Thor",
|
||||
"Medivac", "Wraith", "Viking", "Banshee", "Battlecruiser",
|
||||
"Bunker", "Missile Turret"
|
||||
]
|
||||
|
||||
BARRACKS_UNITS = {"Marine", "Medic", "Firebat", "Marauder", "Reaper", "Ghost", "Spectre"}
|
||||
FACTORY_UNITS = {"Hellion", "Vulture", "Goliath", "Diamondback", "Siege Tank", "Thor", "Predator"}
|
||||
STARPORT_UNITS = {"Medivac", "Wraith", "Viking", "Banshee", "Battlecruiser", "Hercules", "Science Vessel", "Raven"}
|
||||
|
||||
PROTOSS_REGIONS = {"A Sinister Turn", "Echoes of the Future", "In Utter Darkness"}
|
||||
|
||||
|
||||
def filter_missions(world: MultiWorld, player: int) -> Dict[str, List[str]]:
|
||||
"""
|
||||
Returns a semi-randomly pruned tuple of no-build, easy, medium, and hard mission sets
|
||||
"""
|
||||
|
||||
mission_order_type = get_option_value(world, player, "mission_order")
|
||||
shuffle_protoss = get_option_value(world, player, "shuffle_protoss")
|
||||
excluded_missions = set(get_option_set_value(world, player, "excluded_missions"))
|
||||
invalid_mission_names = excluded_missions.difference(vanilla_mission_req_table.keys())
|
||||
if invalid_mission_names:
|
||||
raise Exception("Error in locked_missions - the following are not valid mission names: " + ", ".join(invalid_mission_names))
|
||||
mission_count = len(mission_orders[mission_order_type]) - 1
|
||||
# Vanilla and Vanilla Shuffled use the entire mission pool
|
||||
if mission_count == 28:
|
||||
return {
|
||||
"no_build": no_build_regions_list[:],
|
||||
"easy": easy_regions_list[:],
|
||||
"medium": medium_regions_list[:],
|
||||
"hard": hard_regions_list[:],
|
||||
"all_in": ["All-In"]
|
||||
}
|
||||
|
||||
mission_pools = [
|
||||
[],
|
||||
easy_regions_list,
|
||||
medium_regions_list,
|
||||
hard_regions_list
|
||||
]
|
||||
# Omitting Protoss missions if not shuffling protoss
|
||||
if not shuffle_protoss:
|
||||
excluded_missions = excluded_missions.union(PROTOSS_REGIONS)
|
||||
# Replacing All-In on low mission counts
|
||||
if mission_count < 14:
|
||||
final_mission = world.random.choice([mission for mission in alt_final_mission_locations.keys() if mission not in excluded_missions])
|
||||
excluded_missions.add(final_mission)
|
||||
else:
|
||||
final_mission = 'All-In'
|
||||
# Yaml settings determine which missions can be placed in the first slot
|
||||
mission_pools[0] = [mission for mission in get_starting_mission_locations(world, player).keys() if mission not in excluded_missions]
|
||||
# Removing the new no-build missions from their original sets
|
||||
for i in range(1, len(mission_pools)):
|
||||
mission_pools[i] = [mission for mission in mission_pools[i] if mission not in excluded_missions.union(mission_pools[0])]
|
||||
# If the first mission is a build mission, there may not be enough locations to reach Outbreak as a second mission
|
||||
if not get_option_value(world, player, 'shuffle_no_build'):
|
||||
# Swapping Outbreak and The Great Train Robbery
|
||||
if "Outbreak" in mission_pools[1]:
|
||||
mission_pools[1].remove("Outbreak")
|
||||
mission_pools[2].append("Outbreak")
|
||||
if "The Great Train Robbery" in mission_pools[2]:
|
||||
mission_pools[2].remove("The Great Train Robbery")
|
||||
mission_pools[1].append("The Great Train Robbery")
|
||||
# Removing random missions from each difficulty set in a cycle
|
||||
set_cycle = 0
|
||||
current_count = sum(len(mission_pool) for mission_pool in mission_pools)
|
||||
|
||||
if current_count < mission_count:
|
||||
raise Exception("Not enough missions available to fill the campaign on current settings. Please exclude fewer missions.")
|
||||
while current_count > mission_count:
|
||||
if set_cycle == 4:
|
||||
set_cycle = 0
|
||||
# Must contain at least one mission per set
|
||||
mission_pool = mission_pools[set_cycle]
|
||||
if len(mission_pool) <= 1:
|
||||
if all(len(mission_pool) <= 1 for mission_pool in mission_pools):
|
||||
raise Exception("Not enough missions available to fill the campaign on current settings. Please exclude fewer missions.")
|
||||
else:
|
||||
mission_pool.remove(world.random.choice(mission_pool))
|
||||
current_count -= 1
|
||||
set_cycle += 1
|
||||
|
||||
return {
|
||||
"no_build": mission_pools[0],
|
||||
"easy": mission_pools[1],
|
||||
"medium": mission_pools[2],
|
||||
"hard": mission_pools[3],
|
||||
"all_in": [final_mission]
|
||||
}
|
||||
|
||||
|
||||
def get_item_upgrades(inventory: List[Item], parent_item: Item or str):
|
||||
item_name = parent_item.name if isinstance(parent_item, Item) else parent_item
|
||||
return [
|
||||
inv_item for inv_item in inventory
|
||||
if item_table[inv_item.name].parent_item == item_name
|
||||
]
|
||||
|
||||
|
||||
class ValidInventory:
|
||||
|
||||
def has(self, item: str, player: int):
|
||||
return item in self.logical_inventory
|
||||
|
||||
def has_any(self, items: Set[str], player: int):
|
||||
return any(item in self.logical_inventory for item in items)
|
||||
|
||||
def has_all(self, items: Set[str], player: int):
|
||||
return all(item in self.logical_inventory for item in items)
|
||||
|
||||
def has_units_per_structure(self) -> bool:
|
||||
return len(BARRACKS_UNITS.intersection(self.logical_inventory)) > self.min_units_per_structure and \
|
||||
len(FACTORY_UNITS.intersection(self.logical_inventory)) > self.min_units_per_structure and \
|
||||
len(STARPORT_UNITS.intersection(self.logical_inventory)) > self.min_units_per_structure
|
||||
|
||||
def generate_reduced_inventory(self, inventory_size: int, mission_requirements: List[Callable]) -> List[Item]:
|
||||
"""Attempts to generate a reduced inventory that can fulfill the mission requirements."""
|
||||
inventory = list(self.item_pool)
|
||||
locked_items = list(self.locked_items)
|
||||
self.logical_inventory = {
|
||||
item.name for item in inventory + locked_items + self.existing_items
|
||||
if item.classification in (ItemClassification.progression, ItemClassification.progression_skip_balancing)
|
||||
}
|
||||
requirements = mission_requirements
|
||||
cascade_keys = self.cascade_removal_map.keys()
|
||||
units_always_have_upgrades = get_option_value(self.world, self.player, "units_always_have_upgrades")
|
||||
if self.min_units_per_structure > 0:
|
||||
requirements.append(lambda state: state.has_units_per_structure())
|
||||
|
||||
def attempt_removal(item: Item) -> bool:
|
||||
# If item can be removed and has associated items, remove them as well
|
||||
inventory.remove(item)
|
||||
# Only run logic checks when removing logic items
|
||||
if item.name in self.logical_inventory:
|
||||
self.logical_inventory.remove(item.name)
|
||||
if not all(requirement(self) for requirement in requirements):
|
||||
# If item cannot be removed, lock or revert
|
||||
self.logical_inventory.add(item.name)
|
||||
locked_items.append(item)
|
||||
return False
|
||||
return True
|
||||
|
||||
while len(inventory) + len(locked_items) > inventory_size:
|
||||
if len(inventory) == 0:
|
||||
raise Exception("Reduced item pool generation failed - not enough locations available to place items.")
|
||||
# Select random item from removable items
|
||||
item = self.world.random.choice(inventory)
|
||||
# Cascade removals to associated items
|
||||
if item in cascade_keys:
|
||||
items_to_remove = self.cascade_removal_map[item]
|
||||
transient_items = []
|
||||
while len(items_to_remove) > 0:
|
||||
item_to_remove = items_to_remove.pop()
|
||||
if item_to_remove not in inventory:
|
||||
continue
|
||||
success = attempt_removal(item_to_remove)
|
||||
if success:
|
||||
transient_items.append(item_to_remove)
|
||||
elif units_always_have_upgrades:
|
||||
# Lock all associated items if any of them cannot be removed
|
||||
transient_items += items_to_remove
|
||||
for transient_item in transient_items:
|
||||
if transient_item not in inventory and transient_item not in locked_items:
|
||||
locked_items += transient_item
|
||||
if transient_item.classification in (ItemClassification.progression, ItemClassification.progression_skip_balancing):
|
||||
self.logical_inventory.add(transient_item.name)
|
||||
break
|
||||
else:
|
||||
attempt_removal(item)
|
||||
|
||||
return inventory + locked_items
|
||||
|
||||
def _read_logic(self):
|
||||
self._sc2wol_has_common_unit = lambda world, player: SC2WoLLogic._sc2wol_has_common_unit(self, world, player)
|
||||
self._sc2wol_has_air = lambda world, player: SC2WoLLogic._sc2wol_has_air(self, world, player)
|
||||
self._sc2wol_has_air_anti_air = lambda world, player: SC2WoLLogic._sc2wol_has_air_anti_air(self, world, player)
|
||||
self._sc2wol_has_competent_anti_air = lambda world, player: SC2WoLLogic._sc2wol_has_competent_anti_air(self, world, player)
|
||||
self._sc2wol_has_anti_air = lambda world, player: SC2WoLLogic._sc2wol_has_anti_air(self, world, player)
|
||||
self._sc2wol_defense_rating = lambda world, player, zerg_enemy, air_enemy=False: SC2WoLLogic._sc2wol_defense_rating(self, world, player, zerg_enemy, air_enemy)
|
||||
self._sc2wol_has_competent_comp = lambda world, player: SC2WoLLogic._sc2wol_has_competent_comp(self, world, player)
|
||||
self._sc2wol_has_train_killers = lambda world, player: SC2WoLLogic._sc2wol_has_train_killers(self, world, player)
|
||||
self._sc2wol_able_to_rescue = lambda world, player: SC2WoLLogic._sc2wol_able_to_rescue(self, world, player)
|
||||
self._sc2wol_beats_protoss_deathball = lambda world, player: SC2WoLLogic._sc2wol_beats_protoss_deathball(self, world, player)
|
||||
self._sc2wol_survives_rip_field = lambda world, player: SC2WoLLogic._sc2wol_survives_rip_field(self, world, player)
|
||||
self._sc2wol_has_protoss_common_units = lambda world, player: SC2WoLLogic._sc2wol_has_protoss_common_units(self, world, player)
|
||||
self._sc2wol_has_protoss_medium_units = lambda world, player: SC2WoLLogic._sc2wol_has_protoss_medium_units(self, world, player)
|
||||
self._sc2wol_has_mm_upgrade = lambda world, player: SC2WoLLogic._sc2wol_has_mm_upgrade(self, world, player)
|
||||
self._sc2wol_final_mission_requirements = lambda world, player: SC2WoLLogic._sc2wol_final_mission_requirements(self, world, player)
|
||||
|
||||
def __init__(self, world: MultiWorld, player: int,
|
||||
item_pool: List[Item], existing_items: List[Item], locked_items: List[Item],
|
||||
has_protoss: bool):
|
||||
self.world = world
|
||||
self.player = player
|
||||
self.logical_inventory = set()
|
||||
self.locked_items = locked_items[:]
|
||||
self.existing_items = existing_items
|
||||
self._read_logic()
|
||||
# Initial filter of item pool
|
||||
self.item_pool = []
|
||||
item_quantities: dict[str, int] = dict()
|
||||
# Inventory restrictiveness based on number of missions with checks
|
||||
mission_order_type = get_option_value(self.world, self.player, "mission_order")
|
||||
mission_count = len(mission_orders[mission_order_type]) - 1
|
||||
self.min_units_per_structure = int(mission_count / 7)
|
||||
min_upgrades = 1 if mission_count < 10 else 2
|
||||
for item in item_pool:
|
||||
item_info = item_table[item.name]
|
||||
if item_info.type == "Upgrade":
|
||||
# Locking upgrades based on mission duration
|
||||
if item.name not in item_quantities:
|
||||
item_quantities[item.name] = 0
|
||||
item_quantities[item.name] += 1
|
||||
if item_quantities[item.name] < min_upgrades:
|
||||
self.locked_items.append(item)
|
||||
else:
|
||||
self.item_pool.append(item)
|
||||
elif item_info.type == "Goal":
|
||||
locked_items.append(item)
|
||||
elif item_info.type != "Protoss" or has_protoss:
|
||||
self.item_pool.append(item)
|
||||
self.cascade_removal_map: Dict[Item, List[Item]] = dict()
|
||||
for item in self.item_pool + locked_items + existing_items:
|
||||
if item.name in UPGRADABLE_ITEMS:
|
||||
upgrades = get_item_upgrades(self.item_pool, item)
|
||||
associated_items = [*upgrades, item]
|
||||
self.cascade_removal_map[item] = associated_items
|
||||
if get_option_value(world, player, "units_always_have_upgrades"):
|
||||
for upgrade in upgrades:
|
||||
self.cascade_removal_map[upgrade] = associated_items
|
||||
|
||||
|
||||
def filter_items(world: MultiWorld, player: int, mission_req_table: Dict[str, MissionInfo], location_cache: List[Location],
|
||||
item_pool: List[Item], existing_items: List[Item], locked_items: List[Item]) -> List[Item]:
|
||||
"""
|
||||
Returns a semi-randomly pruned set of items based on number of available locations.
|
||||
The returned inventory must be capable of logically accessing every location in the world.
|
||||
"""
|
||||
open_locations = [location for location in location_cache if location.item is None]
|
||||
inventory_size = len(open_locations)
|
||||
has_protoss = bool(PROTOSS_REGIONS.intersection(mission_req_table.keys()))
|
||||
mission_requirements = [location.access_rule for location in location_cache]
|
||||
valid_inventory = ValidInventory(world, player, item_pool, existing_items, locked_items, has_protoss)
|
||||
|
||||
valid_items = valid_inventory.generate_reduced_inventory(inventory_size, mission_requirements)
|
||||
return valid_items
|
||||
+61
-68
@@ -1,56 +1,48 @@
|
||||
from typing import List, Set, Dict, Tuple, Optional, Callable, NamedTuple
|
||||
from typing import List, Set, Dict, Tuple, Optional, Callable
|
||||
from BaseClasses import MultiWorld, Region, Entrance, Location, RegionType
|
||||
from .Locations import LocationData
|
||||
from .Options import get_option_value
|
||||
from worlds.sc2wol.MissionTables import MissionInfo, vanilla_shuffle_order, vanilla_mission_req_table, \
|
||||
no_build_regions_list, easy_regions_list, medium_regions_list, hard_regions_list
|
||||
from .MissionTables import MissionInfo, mission_orders, vanilla_mission_req_table, alt_final_mission_locations
|
||||
from .PoolFilter import filter_missions
|
||||
import random
|
||||
|
||||
|
||||
def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData, ...], location_cache: List[Location]):
|
||||
def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData, ...], location_cache: List[Location])\
|
||||
-> Tuple[Dict[str, MissionInfo], int, str]:
|
||||
locations_per_region = get_locations_per_region(locations)
|
||||
|
||||
regions = [
|
||||
create_region(world, player, locations_per_region, location_cache, "Menu"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Liberation Day"),
|
||||
create_region(world, player, locations_per_region, location_cache, "The Outlaws"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Zero Hour"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Evacuation"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Outbreak"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Safe Haven"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Haven's Fall"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Smash and Grab"),
|
||||
create_region(world, player, locations_per_region, location_cache, "The Dig"),
|
||||
create_region(world, player, locations_per_region, location_cache, "The Moebius Factor"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Supernova"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Maw of the Void"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Devil's Playground"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Welcome to the Jungle"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Breakout"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Ghost of a Chance"),
|
||||
create_region(world, player, locations_per_region, location_cache, "The Great Train Robbery"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Cutthroat"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Engine of Destruction"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Media Blitz"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Piercing the Shroud"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Whispers of Doom"),
|
||||
create_region(world, player, locations_per_region, location_cache, "A Sinister Turn"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Echoes of the Future"),
|
||||
create_region(world, player, locations_per_region, location_cache, "In Utter Darkness"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Gates of Hell"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Belly of the Beast"),
|
||||
create_region(world, player, locations_per_region, location_cache, "Shatter the Sky"),
|
||||
create_region(world, player, locations_per_region, location_cache, "All-In")
|
||||
]
|
||||
mission_order_type = get_option_value(world, player, "mission_order")
|
||||
mission_order = mission_orders[mission_order_type]
|
||||
|
||||
mission_pools = filter_missions(world, player)
|
||||
final_mission = mission_pools['all_in'][0]
|
||||
|
||||
used_regions = [mission for mission_pool in mission_pools.values() for mission in mission_pool]
|
||||
regions = [create_region(world, player, locations_per_region, location_cache, "Menu")]
|
||||
for region_name in used_regions:
|
||||
regions.append(create_region(world, player, locations_per_region, location_cache, region_name))
|
||||
# Changing the completion condition for alternate final missions into an event
|
||||
if final_mission != 'All-In':
|
||||
final_location = alt_final_mission_locations[final_mission]
|
||||
# Final location should be near the end of the cache
|
||||
for i in range(len(location_cache) - 1, -1, -1):
|
||||
if location_cache[i].name == final_location:
|
||||
location_cache[i].locked = True
|
||||
location_cache[i].event = True
|
||||
location_cache[i].address = None
|
||||
break
|
||||
else:
|
||||
final_location = 'All-In: Victory'
|
||||
|
||||
if __debug__:
|
||||
throwIfAnyLocationIsNotAssignedToARegion(regions, locations_per_region.keys())
|
||||
if mission_order_type in (0, 1):
|
||||
throwIfAnyLocationIsNotAssignedToARegion(regions, locations_per_region.keys())
|
||||
|
||||
world.regions += regions
|
||||
|
||||
names: Dict[str, int] = {}
|
||||
|
||||
if get_option_value(world, player, "mission_order") == 0:
|
||||
if mission_order_type == 0:
|
||||
connect(world, player, names, 'Menu', 'Liberation Day'),
|
||||
connect(world, player, names, 'Liberation Day', 'The Outlaws',
|
||||
lambda state: state.has("Beat Liberation Day", player)),
|
||||
@@ -119,32 +111,30 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData
|
||||
lambda state: state.has('Beat Gates of Hell', player) and (
|
||||
state.has('Beat Shatter the Sky', player) or state.has('Beat Belly of the Beast', player)))
|
||||
|
||||
return vanilla_mission_req_table
|
||||
return vanilla_mission_req_table, 29, final_location
|
||||
|
||||
elif get_option_value(world, player, "mission_order") == 1:
|
||||
else:
|
||||
missions = []
|
||||
no_build_pool = no_build_regions_list[:]
|
||||
easy_pool = easy_regions_list[:]
|
||||
medium_pool = medium_regions_list[:]
|
||||
hard_pool = hard_regions_list[:]
|
||||
|
||||
# Initial fill out of mission list and marking all-in mission
|
||||
for mission in vanilla_shuffle_order:
|
||||
if mission.type == "all_in":
|
||||
missions.append("All-In")
|
||||
elif get_option_value(world, player, "relegate_no_build") and mission.relegate:
|
||||
for mission in mission_order:
|
||||
if mission is None:
|
||||
missions.append(None)
|
||||
elif mission.type == "all_in":
|
||||
missions.append(final_mission)
|
||||
elif mission.relegate and not get_option_value(world, player, "shuffle_no_build"):
|
||||
missions.append("no_build")
|
||||
else:
|
||||
missions.append(mission.type)
|
||||
|
||||
# Place Protoss Missions if we are not using ShuffleProtoss
|
||||
if get_option_value(world, player, "shuffle_protoss") == 0:
|
||||
# Place Protoss Missions if we are not using ShuffleProtoss and are in Vanilla Shuffled
|
||||
if get_option_value(world, player, "shuffle_protoss") == 0 and mission_order_type == 1:
|
||||
missions[22] = "A Sinister Turn"
|
||||
medium_pool.remove("A Sinister Turn")
|
||||
mission_pools['medium'].remove("A Sinister Turn")
|
||||
missions[23] = "Echoes of the Future"
|
||||
medium_pool.remove("Echoes of the Future")
|
||||
mission_pools['medium'].remove("Echoes of the Future")
|
||||
missions[24] = "In Utter Darkness"
|
||||
hard_pool.remove("In Utter Darkness")
|
||||
mission_pools['hard'].remove("In Utter Darkness")
|
||||
|
||||
no_build_slots = []
|
||||
easy_slots = []
|
||||
@@ -153,6 +143,8 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData
|
||||
|
||||
# Search through missions to find slots needed to fill
|
||||
for i in range(len(missions)):
|
||||
if missions[i] is None:
|
||||
continue
|
||||
if missions[i] == "no_build":
|
||||
no_build_slots.append(i)
|
||||
elif missions[i] == "easy":
|
||||
@@ -163,30 +155,30 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData
|
||||
hard_slots.append(i)
|
||||
|
||||
# Add no_build missions to the pool and fill in no_build slots
|
||||
missions_to_add = no_build_pool
|
||||
missions_to_add = mission_pools['no_build']
|
||||
for slot in no_build_slots:
|
||||
filler = random.randint(0, len(missions_to_add)-1)
|
||||
filler = world.random.randint(0, len(missions_to_add)-1)
|
||||
|
||||
missions[slot] = missions_to_add.pop(filler)
|
||||
|
||||
# Add easy missions into pool and fill in easy slots
|
||||
missions_to_add = missions_to_add + easy_pool
|
||||
missions_to_add = missions_to_add + mission_pools['easy']
|
||||
for slot in easy_slots:
|
||||
filler = random.randint(0, len(missions_to_add) - 1)
|
||||
filler = world.random.randint(0, len(missions_to_add) - 1)
|
||||
|
||||
missions[slot] = missions_to_add.pop(filler)
|
||||
|
||||
# Add medium missions into pool and fill in medium slots
|
||||
missions_to_add = missions_to_add + medium_pool
|
||||
missions_to_add = missions_to_add + mission_pools['medium']
|
||||
for slot in medium_slots:
|
||||
filler = random.randint(0, len(missions_to_add) - 1)
|
||||
filler = world.random.randint(0, len(missions_to_add) - 1)
|
||||
|
||||
missions[slot] = missions_to_add.pop(filler)
|
||||
|
||||
# Add hard missions into pool and fill in hard slots
|
||||
missions_to_add = missions_to_add + hard_pool
|
||||
missions_to_add = missions_to_add + mission_pools['hard']
|
||||
for slot in hard_slots:
|
||||
filler = random.randint(0, len(missions_to_add) - 1)
|
||||
filler = world.random.randint(0, len(missions_to_add) - 1)
|
||||
|
||||
missions[slot] = missions_to_add.pop(filler)
|
||||
|
||||
@@ -195,7 +187,7 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData
|
||||
mission_req_table = {}
|
||||
for i in range(len(missions)):
|
||||
connections = []
|
||||
for connection in vanilla_shuffle_order[i].connect_to:
|
||||
for connection in mission_order[i].connect_to:
|
||||
if connection == -1:
|
||||
connect(world, player, names, "Menu", missions[i])
|
||||
else:
|
||||
@@ -203,16 +195,17 @@ def create_regions(world: MultiWorld, player: int, locations: Tuple[LocationData
|
||||
(lambda name, missions_req: (lambda state: state.has(f"Beat {name}", player) and
|
||||
state._sc2wol_cleared_missions(world, player,
|
||||
missions_req)))
|
||||
(missions[connection], vanilla_shuffle_order[i].number))
|
||||
(missions[connection], mission_order[i].number))
|
||||
connections.append(connection + 1)
|
||||
|
||||
mission_req_table.update({missions[i]: MissionInfo(
|
||||
vanilla_mission_req_table[missions[i]].id, vanilla_mission_req_table[missions[i]].extra_locations,
|
||||
connections, vanilla_shuffle_order[i].category, number=vanilla_shuffle_order[i].number,
|
||||
completion_critical=vanilla_shuffle_order[i].completion_critical,
|
||||
or_requirements=vanilla_shuffle_order[i].or_requirements)})
|
||||
vanilla_mission_req_table[missions[i]].id, connections, mission_order[i].category,
|
||||
number=mission_order[i].number,
|
||||
completion_critical=mission_order[i].completion_critical,
|
||||
or_requirements=mission_order[i].or_requirements)})
|
||||
|
||||
return mission_req_table
|
||||
final_mission_id = vanilla_mission_req_table[final_mission].id
|
||||
return mission_req_table, final_mission_id, final_mission + ': Victory'
|
||||
|
||||
|
||||
def throwIfAnyLocationIsNotAssignedToARegion(regions: List[Region], regionNames: Set[str]):
|
||||
|
||||
+62
-32
@@ -1,15 +1,16 @@
|
||||
import typing
|
||||
|
||||
from typing import List, Set, Tuple, NamedTuple
|
||||
from typing import List, Set, Tuple, Dict
|
||||
from BaseClasses import Item, MultiWorld, Location, Tutorial, ItemClassification
|
||||
from ..AutoWorld import WebWorld
|
||||
from worlds.AutoWorld import WebWorld, World
|
||||
from .Items import StarcraftWoLItem, item_table, filler_items, item_name_groups, get_full_item_list, \
|
||||
basic_unit
|
||||
get_basic_units
|
||||
from .Locations import get_locations
|
||||
from .Regions import create_regions
|
||||
from .Options import sc2wol_options, get_option_value
|
||||
from .Options import sc2wol_options, get_option_value, get_option_set_value
|
||||
from .LogicMixin import SC2WoLLogic
|
||||
from ..AutoWorld import World
|
||||
from .PoolFilter import filter_missions, filter_items, get_item_upgrades
|
||||
from .MissionTables import get_starting_mission_locations, MissionInfo
|
||||
|
||||
|
||||
class Starcraft2WoLWebWorld(WebWorld):
|
||||
@@ -43,6 +44,8 @@ class SC2WoLWorld(World):
|
||||
locked_locations: typing.List[str]
|
||||
location_cache: typing.List[Location]
|
||||
mission_req_table = {}
|
||||
final_mission_id: int
|
||||
victory_item: str
|
||||
required_client_version = 0, 3, 5
|
||||
|
||||
def __init__(self, world: MultiWorld, player: int):
|
||||
@@ -50,24 +53,21 @@ class SC2WoLWorld(World):
|
||||
self.location_cache = []
|
||||
self.locked_locations = []
|
||||
|
||||
def _create_items(self, name: str):
|
||||
data = get_full_item_list()[name]
|
||||
return [self.create_item(name) for _ in range(data.quantity)]
|
||||
|
||||
def create_item(self, name: str) -> Item:
|
||||
data = get_full_item_list()[name]
|
||||
return StarcraftWoLItem(name, data.classification, data.code, self.player)
|
||||
|
||||
def create_regions(self):
|
||||
self.mission_req_table = create_regions(self.world, self.player, get_locations(self.world, self.player),
|
||||
self.location_cache)
|
||||
self.mission_req_table, self.final_mission_id, self.victory_item = create_regions(
|
||||
self.world, self.player, get_locations(self.world, self.player), self.location_cache
|
||||
)
|
||||
|
||||
def generate_basic(self):
|
||||
excluded_items = get_excluded_items(self, self.world, self.player)
|
||||
|
||||
assign_starter_items(self.world, self.player, excluded_items, self.locked_locations)
|
||||
starter_items = assign_starter_items(self.world, self.player, excluded_items, self.locked_locations)
|
||||
|
||||
pool = get_item_pool(self.world, self.player, excluded_items)
|
||||
pool = get_item_pool(self.world, self.player, self.mission_req_table, starter_items, excluded_items, self.location_cache)
|
||||
|
||||
fill_item_pool_with_dummy_items(self, self.world, self.player, self.locked_locations, self.location_cache, pool)
|
||||
|
||||
@@ -75,8 +75,7 @@ class SC2WoLWorld(World):
|
||||
|
||||
def set_rules(self):
|
||||
setup_events(self.world, self.player, self.locked_locations, self.location_cache)
|
||||
|
||||
self.world.completion_condition[self.player] = lambda state: state.has('All-In: Victory', self.player)
|
||||
self.world.completion_condition[self.player] = lambda state: state.has(self.victory_item, self.player)
|
||||
|
||||
def get_filler_item_name(self) -> str:
|
||||
return self.world.random.choice(filler_items)
|
||||
@@ -92,6 +91,7 @@ class SC2WoLWorld(World):
|
||||
slot_req_table[mission] = self.mission_req_table[mission]._asdict()
|
||||
|
||||
slot_data["mission_req"] = slot_req_table
|
||||
slot_data["final_mission"] = self.final_mission_id
|
||||
return slot_data
|
||||
|
||||
|
||||
@@ -121,30 +121,37 @@ def get_excluded_items(self: SC2WoLWorld, world: MultiWorld, player: int) -> Set
|
||||
for item in world.precollected_items[player]:
|
||||
excluded_items.add(item.name)
|
||||
|
||||
excluded_items_option = getattr(world, 'excluded_items', [])
|
||||
|
||||
excluded_items.update(excluded_items_option[player].value)
|
||||
|
||||
return excluded_items
|
||||
|
||||
|
||||
def assign_starter_items(world: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str]):
|
||||
def assign_starter_items(world: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str]) -> List[Item]:
|
||||
non_local_items = world.non_local_items[player].value
|
||||
if get_option_value(world, player, "early_unit"):
|
||||
local_basic_unit = tuple(item for item in get_basic_units(world, player) if item not in non_local_items)
|
||||
if not local_basic_unit:
|
||||
raise Exception("At least one basic unit must be local")
|
||||
|
||||
local_basic_unit = tuple(item for item in basic_unit if item not in non_local_items)
|
||||
if not local_basic_unit:
|
||||
raise Exception("At least one basic unit must be local")
|
||||
# The first world should also be the starting world
|
||||
first_mission = list(world.worlds[player].mission_req_table)[0]
|
||||
starting_mission_locations = get_starting_mission_locations(world, player)
|
||||
if first_mission in starting_mission_locations:
|
||||
first_location = starting_mission_locations[first_mission]
|
||||
elif first_mission == "In Utter Darkness":
|
||||
first_location = first_mission + ": Defeat"
|
||||
else:
|
||||
first_location = first_mission + ": Victory"
|
||||
|
||||
# The first world should also be the starting world
|
||||
first_location = list(world.worlds[player].mission_req_table)[0]
|
||||
|
||||
if first_location == "In Utter Darkness":
|
||||
first_location = first_location + ": Defeat"
|
||||
return [assign_starter_item(world, player, excluded_items, locked_locations, first_location, local_basic_unit)]
|
||||
else:
|
||||
first_location = first_location + ": Victory"
|
||||
|
||||
assign_starter_item(world, player, excluded_items, locked_locations, first_location,
|
||||
local_basic_unit)
|
||||
return []
|
||||
|
||||
|
||||
def assign_starter_item(world: MultiWorld, player: int, excluded_items: Set[str], locked_locations: List[str],
|
||||
location: str, item_list: Tuple[str, ...]):
|
||||
location: str, item_list: Tuple[str, ...]) -> Item:
|
||||
|
||||
item_name = world.random.choice(item_list)
|
||||
|
||||
@@ -156,17 +163,40 @@ def assign_starter_item(world: MultiWorld, player: int, excluded_items: Set[str]
|
||||
|
||||
locked_locations.append(location)
|
||||
|
||||
return item
|
||||
|
||||
def get_item_pool(world: MultiWorld, player: int, excluded_items: Set[str]) -> List[Item]:
|
||||
|
||||
def get_item_pool(world: MultiWorld, player: int, mission_req_table: Dict[str, MissionInfo],
|
||||
starter_items: List[str], excluded_items: Set[str], location_cache: List[Location]) -> List[Item]:
|
||||
pool: List[Item] = []
|
||||
|
||||
# For the future: goal items like Artifact Shards go here
|
||||
locked_items = []
|
||||
|
||||
# YAML items
|
||||
yaml_locked_items = get_option_set_value(world, player, 'locked_items')
|
||||
|
||||
for name, data in item_table.items():
|
||||
if name not in excluded_items:
|
||||
for _ in range(data.quantity):
|
||||
item = create_item_with_correct_settings(world, player, name)
|
||||
pool.append(item)
|
||||
if name in yaml_locked_items:
|
||||
locked_items.append(item)
|
||||
else:
|
||||
pool.append(item)
|
||||
|
||||
return pool
|
||||
existing_items = starter_items + [item for item in world.precollected_items[player]]
|
||||
existing_names = [item.name for item in existing_items]
|
||||
# Removing upgrades for excluded items
|
||||
for item_name in excluded_items:
|
||||
if item_name in existing_names:
|
||||
continue
|
||||
invalid_upgrades = get_item_upgrades(pool, item_name)
|
||||
for invalid_upgrade in invalid_upgrades:
|
||||
pool.remove(invalid_upgrade)
|
||||
|
||||
filtered_pool = filter_items(world, player, mission_req_table, location_cache, pool, existing_items, locked_items)
|
||||
return filtered_pool
|
||||
|
||||
|
||||
def fill_item_pool_with_dummy_items(self: SC2WoLWorld, world: MultiWorld, player: int, locked_locations: List[str],
|
||||
|
||||
@@ -15,7 +15,7 @@ missions. When you receive items, they will immediately become available, even d
|
||||
notified via a text box in the top-right corner of the game screen. (The text client for StarCraft 2 also records all
|
||||
items in all worlds.)
|
||||
|
||||
Missions are launched only through the text client. The Hyperion is never visited. Aditionally, credits are not used.
|
||||
Missions are launched only through the text client. The Hyperion is never visited. Additionally, credits are not used.
|
||||
|
||||
## What is the goal of this game when randomized?
|
||||
|
||||
|
||||
@@ -13,9 +13,10 @@ to obtain a config file for StarCraft 2.
|
||||
|
||||
1. Install StarCraft 2 and Archipelago using the first two links above. (The StarCraft 2 client for Archipelago is
|
||||
included by default.)
|
||||
2. Click the third link above and follow the instructions there.
|
||||
3. Linux users should also follow the instructions found at the bottom of this page
|
||||
(["Running in Linux"](#running-in-linux)).
|
||||
- Linux users should also follow the instructions found at the bottom of this page
|
||||
(["Running in Linux"](#running-in-linux)).
|
||||
2. Run ArchipelagoStarcraft2Client.exe.
|
||||
3. Type the command `/download_data`. This will automatically install the Maps and Data files from the third link above.
|
||||
|
||||
## Where do I get a config file (aka "YAML") for this game?
|
||||
|
||||
@@ -40,16 +41,9 @@ Check out [Creating a YAML](https://archipelago.gg/tutorial/Archipelago/setup/en
|
||||
|
||||
## The game isn't launching when I try to start a mission.
|
||||
|
||||
First, check the log file for issues (stored at `[Archipelago Directory]/logs/SC2Client.txt`). If the below fix doesn't
|
||||
work for you, and you can't figure out the log file, visit our [Discord's](https://discord.com/invite/8Z65BR2)
|
||||
tech-support channel for help. Please include a specific description of what's going wrong and attach your log file to
|
||||
your message.
|
||||
|
||||
### Check your installation
|
||||
|
||||
Make sure you've followed the installation instructions completely. Specifically, make sure that you've placed the Maps
|
||||
and Mods folders directly inside the StarCraft II installation folder. They should be in the same location as the
|
||||
SC2Data, Support, Support64, and Versions folders.
|
||||
First, check the log file for issues (stored at `[Archipelago Directory]/logs/SC2Client.txt`). If you can't figure out
|
||||
the log file, visit our [Discord's](https://discord.com/invite/8Z65BR2) tech-support channel for help. Please include a
|
||||
specific description of what's going wrong and attach your log file to your message.
|
||||
|
||||
## Running in Linux
|
||||
|
||||
|
||||
@@ -0,0 +1,158 @@
|
||||
import logging
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
from NetUtils import ClientStatus, color
|
||||
from worlds.AutoSNIClient import SNIClient
|
||||
from .Rom import ROM_PLAYER_LIMIT as SM_ROM_PLAYER_LIMIT
|
||||
|
||||
snes_logger = logging.getLogger("SNES")
|
||||
|
||||
GAME_SM = "Super Metroid"
|
||||
|
||||
# FXPAK Pro protocol memory mapping used by SNI
|
||||
ROM_START = 0x000000
|
||||
WRAM_START = 0xF50000
|
||||
WRAM_SIZE = 0x20000
|
||||
SRAM_START = 0xE00000
|
||||
|
||||
# SM
|
||||
SM_ROMNAME_START = ROM_START + 0x007FC0
|
||||
ROMNAME_SIZE = 0x15
|
||||
|
||||
SM_INGAME_MODES = {0x07, 0x09, 0x0b}
|
||||
SM_ENDGAME_MODES = {0x26, 0x27}
|
||||
SM_DEATH_MODES = {0x13, 0x14, 0x15, 0x16, 0x17, 0x18, 0x19, 0x1A}
|
||||
|
||||
# RECV and SEND are from the gameplay's perspective: SNIClient writes to RECV queue and reads from SEND queue
|
||||
SM_RECV_QUEUE_START = SRAM_START + 0x2000
|
||||
SM_RECV_QUEUE_WCOUNT = SRAM_START + 0x2602
|
||||
SM_SEND_QUEUE_START = SRAM_START + 0x2700
|
||||
SM_SEND_QUEUE_RCOUNT = SRAM_START + 0x2680
|
||||
SM_SEND_QUEUE_WCOUNT = SRAM_START + 0x2682
|
||||
|
||||
SM_DEATH_LINK_ACTIVE_ADDR = ROM_START + 0x277F04 # 1 byte
|
||||
SM_REMOTE_ITEM_FLAG_ADDR = ROM_START + 0x277F06 # 1 byte
|
||||
|
||||
|
||||
class SMSNIClient(SNIClient):
|
||||
game = "Super Metroid"
|
||||
|
||||
async def deathlink_kill_player(self, ctx):
|
||||
from SNIClient import DeathState, snes_buffered_write, snes_flush_writes, snes_read
|
||||
snes_buffered_write(ctx, WRAM_START + 0x09C2, bytes([1, 0])) # set current health to 1 (to prevent saving with 0 energy)
|
||||
snes_buffered_write(ctx, WRAM_START + 0x0A50, bytes([255])) # deal 255 of damage at next opportunity
|
||||
if not ctx.death_link_allow_survive:
|
||||
snes_buffered_write(ctx, WRAM_START + 0x09D6, bytes([0, 0])) # set current reserve to 0
|
||||
|
||||
await snes_flush_writes(ctx)
|
||||
await asyncio.sleep(1)
|
||||
|
||||
gamemode = await snes_read(ctx, WRAM_START + 0x0998, 1)
|
||||
health = await snes_read(ctx, WRAM_START + 0x09C2, 2)
|
||||
if health is not None:
|
||||
health = health[0] | (health[1] << 8)
|
||||
if not gamemode or gamemode[0] in SM_DEATH_MODES or (
|
||||
ctx.death_link_allow_survive and health is not None and health > 0):
|
||||
ctx.death_state = DeathState.dead
|
||||
|
||||
|
||||
async def validate_rom(self, ctx):
|
||||
from SNIClient import snes_buffered_write, snes_flush_writes, snes_read
|
||||
|
||||
rom_name = await snes_read(ctx, SM_ROMNAME_START, ROMNAME_SIZE)
|
||||
if rom_name is None or rom_name == bytes([0] * ROMNAME_SIZE) or rom_name[:2] != b"SM" or rom_name[:3] == b"SMW":
|
||||
return False
|
||||
|
||||
ctx.game = self.game
|
||||
|
||||
# versions lower than 0.3.0 dont have item handling flag nor remote item support
|
||||
romVersion = int(rom_name[2:5].decode('UTF-8'))
|
||||
if romVersion < 30:
|
||||
ctx.items_handling = 0b001 # full local
|
||||
else:
|
||||
item_handling = await snes_read(ctx, SM_REMOTE_ITEM_FLAG_ADDR, 1)
|
||||
ctx.items_handling = 0b001 if item_handling is None else item_handling[0]
|
||||
|
||||
ctx.rom = rom_name
|
||||
|
||||
death_link = await snes_read(ctx, SM_DEATH_LINK_ACTIVE_ADDR, 1)
|
||||
|
||||
if death_link:
|
||||
ctx.allow_collect = bool(death_link[0] & 0b100)
|
||||
ctx.death_link_allow_survive = bool(death_link[0] & 0b10)
|
||||
await ctx.update_death_link(bool(death_link[0] & 0b1))
|
||||
|
||||
return True
|
||||
|
||||
|
||||
async def game_watcher(self, ctx):
|
||||
from SNIClient import snes_buffered_write, snes_flush_writes, snes_read
|
||||
if ctx.server is None or ctx.slot is None:
|
||||
# not successfully connected to a multiworld server, cannot process the game sending items
|
||||
return
|
||||
|
||||
gamemode = await snes_read(ctx, WRAM_START + 0x0998, 1)
|
||||
if "DeathLink" in ctx.tags and gamemode and ctx.last_death_link + 1 < time.time():
|
||||
currently_dead = gamemode[0] in SM_DEATH_MODES
|
||||
await ctx.handle_deathlink_state(currently_dead)
|
||||
if gamemode is not None and gamemode[0] in SM_ENDGAME_MODES:
|
||||
if not ctx.finished_game:
|
||||
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
||||
ctx.finished_game = True
|
||||
return
|
||||
|
||||
data = await snes_read(ctx, SM_SEND_QUEUE_RCOUNT, 4)
|
||||
if data is None:
|
||||
return
|
||||
|
||||
recv_index = data[0] | (data[1] << 8)
|
||||
recv_item = data[2] | (data[3] << 8) # this is actually SM_SEND_QUEUE_WCOUNT
|
||||
|
||||
while (recv_index < recv_item):
|
||||
item_address = recv_index * 8
|
||||
message = await snes_read(ctx, SM_SEND_QUEUE_START + item_address, 8)
|
||||
item_index = (message[4] | (message[5] << 8)) >> 3
|
||||
|
||||
recv_index += 1
|
||||
snes_buffered_write(ctx, SM_SEND_QUEUE_RCOUNT,
|
||||
bytes([recv_index & 0xFF, (recv_index >> 8) & 0xFF]))
|
||||
|
||||
from worlds.sm import locations_start_id
|
||||
location_id = locations_start_id + item_index
|
||||
|
||||
ctx.locations_checked.add(location_id)
|
||||
location = ctx.location_names[location_id]
|
||||
snes_logger.info(
|
||||
f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})')
|
||||
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [location_id]}])
|
||||
|
||||
data = await snes_read(ctx, SM_RECV_QUEUE_WCOUNT, 2)
|
||||
if data is None:
|
||||
return
|
||||
|
||||
item_out_ptr = data[0] | (data[1] << 8)
|
||||
|
||||
from worlds.sm import items_start_id
|
||||
from worlds.sm import locations_start_id
|
||||
if item_out_ptr < len(ctx.items_received):
|
||||
item = ctx.items_received[item_out_ptr]
|
||||
item_id = item.item - items_start_id
|
||||
if bool(ctx.items_handling & 0b010):
|
||||
location_id = (item.location - locations_start_id) if (item.location >= 0 and item.player == ctx.slot) else 0xFF
|
||||
else:
|
||||
location_id = 0x00 #backward compat
|
||||
|
||||
player_id = item.player if item.player <= SM_ROM_PLAYER_LIMIT else 0
|
||||
snes_buffered_write(ctx, SM_RECV_QUEUE_START + item_out_ptr * 4, bytes(
|
||||
[player_id & 0xFF, (player_id >> 8) & 0xFF, item_id & 0xFF, location_id & 0xFF]))
|
||||
item_out_ptr += 1
|
||||
snes_buffered_write(ctx, SM_RECV_QUEUE_WCOUNT,
|
||||
bytes([item_out_ptr & 0xFF, (item_out_ptr >> 8) & 0xFF]))
|
||||
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
|
||||
color(ctx.item_names[item.item], 'red', 'bold'),
|
||||
color(ctx.player_names[item.player], 'yellow'),
|
||||
ctx.location_names[item.location], item_out_ptr, len(ctx.items_received)))
|
||||
|
||||
await snes_flush_writes(ctx)
|
||||
|
||||
+3
-2
@@ -3,7 +3,8 @@ import os
|
||||
|
||||
import json
|
||||
import Utils
|
||||
from Patch import read_rom, APDeltaPatch
|
||||
from Utils import read_snes_rom
|
||||
from worlds.Files import APDeltaPatch
|
||||
|
||||
SMJUHASH = '21f3e98df4780ee1c667b84e57d88675'
|
||||
ROM_PLAYER_LIMIT = 65535 # max archipelago player ID. note, SM ROM itself will only store 201 names+ids max
|
||||
@@ -22,7 +23,7 @@ def get_base_rom_bytes(file_name: str = "") -> bytes:
|
||||
base_rom_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 = bytes(read_rom(open(file_name, "rb")))
|
||||
base_rom_bytes = bytes(read_snes_rom(open(file_name, "rb")))
|
||||
|
||||
basemd5 = hashlib.md5()
|
||||
basemd5.update(base_rom_bytes)
|
||||
|
||||
+3
-10
@@ -14,6 +14,7 @@ logger = logging.getLogger("Super Metroid")
|
||||
from .Regions import create_regions
|
||||
from .Rules import set_rules, add_entrance_rule
|
||||
from .Options import sm_options
|
||||
from .Client import SMSNIClient
|
||||
from .Rom import get_base_rom_path, ROM_PLAYER_LIMIT, SMDeltaPatch, get_sm_symbols
|
||||
import Utils
|
||||
|
||||
@@ -505,10 +506,8 @@ class SMWorld(World):
|
||||
romPatcher.writeRandoSettings(self.variaRando.randoExec.randoSettings, itemLocs)
|
||||
|
||||
def generate_output(self, output_directory: str):
|
||||
outfilebase = 'AP_' + self.world.seed_name
|
||||
outfilepname = f'_P{self.player}'
|
||||
outfilepname += f"_{self.world.get_file_safe_player_name(self.player).replace(' ', '_')}"
|
||||
outputFilename = os.path.join(output_directory, f'{outfilebase}{outfilepname}.sfc')
|
||||
outfilebase = self.world.get_out_file_name_base(self.player)
|
||||
outputFilename = os.path.join(output_directory, f"{outfilebase}.sfc")
|
||||
|
||||
try:
|
||||
self.variaRando.PatchRom(outputFilename, self.APPrePatchRom, self.APPostPatchRom)
|
||||
@@ -659,12 +658,6 @@ class SMWorld(World):
|
||||
loc.place_locked_item(item)
|
||||
loc.address = loc.item.code = None
|
||||
|
||||
@classmethod
|
||||
def stage_fill_hook(cls, world, progitempool, usefulitempool, filleritempool, fill_locations):
|
||||
if world.get_game_players("Super Metroid"):
|
||||
progitempool.sort(
|
||||
key=lambda item: 1 if (item.name == 'Morph Ball') else 0)
|
||||
|
||||
@classmethod
|
||||
def stage_post_fill(cls, world):
|
||||
new_state = CollectionState(world)
|
||||
|
||||
@@ -7,8 +7,7 @@
|
||||
|
||||
- Hardware or software capable of loading and playing SNES ROM files
|
||||
- An emulator capable of connecting to SNI such as:
|
||||
- snes9x Multitroid
|
||||
from: [snes9x Multitroid Download](https://drive.google.com/drive/folders/1_ej-pwWtCAHYXIrvs5Hro16A1s9Hi3Jz),
|
||||
- snes9x-rr from: [snes9x rr](https://github.com/gocha/snes9x-rr/releases),
|
||||
- BizHawk from: [BizHawk Website](http://tasvideos.org/BizHawk.html)
|
||||
- RetroArch 1.10.1 or newer from: [RetroArch Website](https://retroarch.com?page=platforms). Or,
|
||||
- An SD2SNES, FXPak Pro ([FXPak Pro Store Page](https://krikzz.com/store/home/54-fxpak-pro.html)), or other
|
||||
@@ -81,7 +80,7 @@ client, and will also create your ROM in the same place as your patch file.
|
||||
When the client launched automatically, SNI should have also automatically launched in the background. If this is its
|
||||
first time launching, you may be prompted to allow it to communicate through the Windows Firewall.
|
||||
|
||||
##### snes9x Multitroid
|
||||
##### snes9x-rr
|
||||
|
||||
1. Load your ROM file if it hasn't already been loaded.
|
||||
2. Click on the File menu and hover on **Lua Scripting**
|
||||
|
||||
@@ -46,7 +46,7 @@ class MIPS1Cost(Range):
|
||||
|
||||
|
||||
class MIPS2Cost(Range):
|
||||
"""How many stars are required to spawn MIPS the secound time. Must be bigger or equal MIPS1Cost"""
|
||||
"""How many stars are required to spawn MIPS the second time."""
|
||||
range_start = 0
|
||||
range_end = 80
|
||||
default = 50
|
||||
@@ -72,7 +72,8 @@ class AreaRandomizer(Choice):
|
||||
display_name = "Entrance Randomizer"
|
||||
option_Off = 0
|
||||
option_Courses_Only = 1
|
||||
option_Courses_and_Secrets = 2
|
||||
option_Courses_and_Secrets_Separate = 2
|
||||
option_Courses_and_Secrets = 3
|
||||
|
||||
|
||||
class BuddyChecks(Toggle):
|
||||
|
||||
@@ -11,13 +11,14 @@ def fix_reg(entrance_ids, reg, invalidspot, swaplist, world):
|
||||
|
||||
def set_rules(world, player: int, area_connections):
|
||||
destination_regions = list(range(13)) + [12,13,14] + list(range(15,15+len(sm64secrets))) # Two instances of Destination Course THI. Past normal course idx are secret regions
|
||||
if world.AreaRandomizer[player].value == 0:
|
||||
entrance_ids = list(range(len(sm64paintings + sm64secrets)))
|
||||
if world.AreaRandomizer[player].value >= 1: # Some randomization is happening, randomize Courses
|
||||
entrance_ids = list(range(len(sm64paintings)))
|
||||
world.random.shuffle(entrance_ids)
|
||||
entrance_ids = entrance_ids + list(range(len(sm64paintings), len(sm64paintings) + len(sm64secrets)))
|
||||
if world.AreaRandomizer[player].value == 2: # Secret Regions as well
|
||||
secret_entrance_ids = list(range(len(sm64paintings), len(sm64paintings) + len(sm64secrets)))
|
||||
course_entrance_ids = list(range(len(sm64paintings)))
|
||||
if world.AreaRandomizer[player].value >= 1: # Some randomization is happening, randomize Courses
|
||||
world.random.shuffle(course_entrance_ids)
|
||||
if world.AreaRandomizer[player].value == 2: # Randomize Secrets as well
|
||||
world.random.shuffle(secret_entrance_ids)
|
||||
entrance_ids = course_entrance_ids + secret_entrance_ids
|
||||
if world.AreaRandomizer[player].value == 3: # Randomize Courses and Secrets in one pool
|
||||
world.random.shuffle(entrance_ids)
|
||||
# Guarantee first entrance is a course
|
||||
swaplist = list(range(len(entrance_ids)))
|
||||
@@ -117,7 +118,7 @@ def set_rules(world, player: int, area_connections):
|
||||
add_rule(world.get_location("Toad (Third Floor)", player), lambda state: state.can_reach("Third Floor", 'Region', player) and state.has("Power Star", player, 35))
|
||||
|
||||
if world.MIPS1Cost[player].value > world.MIPS2Cost[player].value:
|
||||
world.MIPS2Cost[player].value = world.MIPS1Cost[player].value
|
||||
(world.MIPS2Cost[player].value, world.MIPS1Cost[player].value) = (world.MIPS1Cost[player].value, world.MIPS2Cost[player].value)
|
||||
add_rule(world.get_location("MIPS 1", player), lambda state: state.can_reach("Basement", 'Region', player) and state.has("Power Star", player, world.MIPS1Cost[player].value))
|
||||
add_rule(world.get_location("MIPS 2", player), lambda state: state.can_reach("Basement", 'Region', player) and state.has("Power Star", player, world.MIPS2Cost[player].value))
|
||||
|
||||
|
||||
@@ -36,7 +36,7 @@ class SM64World(World):
|
||||
location_name_to_id = location_table
|
||||
|
||||
data_version = 8
|
||||
required_client_version = (0, 3, 0)
|
||||
required_client_version = (0, 3, 5)
|
||||
|
||||
area_connections: typing.Dict[int, int]
|
||||
|
||||
@@ -173,7 +173,7 @@ class SM64World(World):
|
||||
}
|
||||
}
|
||||
}
|
||||
filename = f"AP_{self.world.seed_name}_P{self.player}_{self.world.get_file_safe_player_name(self.player)}.apsm64ex"
|
||||
filename = f"{self.world.get_out_file_name_base(self.player)}.apsm64ex"
|
||||
with open(os.path.join(output_directory, filename), 'w') as f:
|
||||
json.dump(data, f)
|
||||
|
||||
|
||||
@@ -3,8 +3,10 @@
|
||||
## Required Software
|
||||
|
||||
- Super Mario 64 US Rom (Japanese may work also. Europe and Shindou not supported)
|
||||
- Either of [sm64pclauncher](https://github.com/N00byKing/sm64pclauncher/releases) or
|
||||
- Cloning and building [sm64ex](https://github.com/N00byKing/sm64ex) manually.
|
||||
- Either of
|
||||
- [sm64pclauncher](https://github.com/N00byKing/sm64pclauncher/releases) or
|
||||
- Cloning and building [sm64ex](https://github.com/N00byKing/sm64ex) manually
|
||||
- Optional, for sending [commands](/tutorial/Archipelago/commands/en) like `!hint`: the TextClient from [the most recent Archipelago release](https://github.com/ArchipelagoMW/Archipelago/releases)
|
||||
|
||||
NOTE: The above linked sm64pclauncher is a special version designed to work with the Archipelago build of sm64ex.
|
||||
You can use other sm64-port based builds with it, but you can't use a different launcher with the Archipelago build of sm64ex.
|
||||
@@ -25,7 +27,9 @@ Then follow the steps below
|
||||
6. Set the location where you installed MSYS when prompted. Check the "Install Dependencies" Checkbox
|
||||
7. Set the Repo link to `https://github.com/N00byKing/sm64ex` and the Branch to `archipelago` (Top two boxes). You can choose the folder (Secound Box) at will, as long as it does not exist yet
|
||||
8. Point the Launcher to your Super Mario 64 US/JP Rom, and set the Region correspondingly
|
||||
9. Set Build Options. Recommended: `-jn` where `n` is the Number of CPU Cores, to build faster.
|
||||
9. Set Build Options and press build.
|
||||
- Recommended: To build faster, use `-jn` where `n` is the number of CPU cores to use (e.g., `-j4` to use 4 cores).
|
||||
- Optional: Add options from [this list](https://github.com/sm64pc/sm64ex/wiki/Build-options), separated by spaces (e.g., `-j4 BETTERCAMERA=1`).
|
||||
10. SM64EX will now be compiled. The Launcher will appear to have crashed, but this is not likely the case. Best wait a bit, but there may be a problem if it takes longer than 10 Minutes
|
||||
|
||||
After it's done, the Build list should have another entry titled with what you named the folder in step 7.
|
||||
@@ -52,7 +56,11 @@ Optionally, add `--sm64ap_passwd "YourPassword"` if the room you are using requi
|
||||
The Name in this case is the one specified in your generated .yaml file.
|
||||
In case you are using the Archipelago Website, the IP should be `archipelago.gg`.
|
||||
|
||||
If everything worked out, you will see a textbox informing you the connection has been established after the story intro.
|
||||
Should the connection fail (for example when using the wrong name or IP/Port combination) the game will inform you of that.
|
||||
Additionally, any time the game is not connected (for example when the connection is unstable) it will attempt to reconnect and display a status text.
|
||||
|
||||
**Important:** You must start a new file for every new seed you play. Using `⭐x0` files is **not** sufficient.
|
||||
Failing to use a new file may make some locations unavailable. However, this can be fixed without losing any progress by exiting and starting a new file.
|
||||
|
||||
# Playing offline
|
||||
|
||||
@@ -82,6 +90,11 @@ with its name.
|
||||
When using a US Rom, the In-Game messages are missing some letters: `J Q V X Z` and `?`.
|
||||
The Japanese Version should have no problem displaying these.
|
||||
|
||||
### Toad does not have an item for me.
|
||||
|
||||
This happens when you load an existing file that had already received an item from that toad.
|
||||
To resolve this, exit and start from a `NEW` file. The server will automatically restore your progress.
|
||||
|
||||
### What happens if I lose connection?
|
||||
|
||||
SM64EX tries to reconnect a few times, so be patient.
|
||||
|
||||
@@ -0,0 +1,202 @@
|
||||
|
||||
mario_palettes = [
|
||||
[0x5F, 0x63, 0x1D, 0x58, 0x0A, 0x00, 0x1F, 0x39, 0xC4, 0x44, 0x08, 0x4E, 0x70, 0x67, 0xB6, 0x30, 0xDF, 0x35, 0xFF, 0x03], # Mario
|
||||
[0x3F, 0x4F, 0x1D, 0x58, 0x40, 0x11, 0xE0, 0x3F, 0x07, 0x3C, 0xAE, 0x7C, 0xB3, 0x7D, 0x00, 0x2F, 0x5F, 0x16, 0xFF, 0x03], # Luigi
|
||||
[0x5F, 0x63, 0x1D, 0x58, 0x0A, 0x00, 0x1F, 0x03, 0xC4, 0x44, 0x08, 0x4E, 0x70, 0x67, 0x16, 0x02, 0xDF, 0x35, 0xFF, 0x03], # Wario
|
||||
[0x5F, 0x63, 0x1D, 0x58, 0x0A, 0x00, 0x12, 0x7C, 0xC4, 0x44, 0x08, 0x4E, 0x70, 0x67, 0x0D, 0x58, 0xDF, 0x35, 0xFF, 0x03], # Waluigi
|
||||
[0x5F, 0x63, 0x1D, 0x58, 0x0A, 0x00, 0x00, 0x7C, 0xC4, 0x44, 0x08, 0x4E, 0x70, 0x67, 0x00, 0x58, 0xDF, 0x35, 0xFF, 0x03], # Geno
|
||||
[0x5F, 0x63, 0x1D, 0x58, 0x0A, 0x00, 0x1F, 0x7C, 0xC4, 0x44, 0x08, 0x4E, 0x70, 0x67, 0x16, 0x58, 0xDF, 0x35, 0xFF, 0x03], # Princess
|
||||
[0x5F, 0x63, 0x1D, 0x58, 0x0A, 0x00, 0xE0, 0x00, 0xC4, 0x44, 0x08, 0x4E, 0x70, 0x67, 0x80, 0x00, 0xDF, 0x35, 0xFF, 0x03], # Dark
|
||||
[0x5F, 0x63, 0x1D, 0x58, 0x0A, 0x00, 0xFF, 0x01, 0xC4, 0x44, 0x08, 0x4E, 0x70, 0x67, 0x5F, 0x01, 0xDF, 0x35, 0xFF, 0x03], # Sponge
|
||||
]
|
||||
|
||||
fire_mario_palettes = [
|
||||
[0x5F, 0x63, 0x1D, 0x58, 0x29, 0x25, 0xFF, 0x7F, 0x08, 0x00, 0x17, 0x00, 0x1F, 0x00, 0x7B, 0x57, 0xDF, 0x0D, 0xFF, 0x03], # Mario
|
||||
[0x1F, 0x3B, 0x1D, 0x58, 0x29, 0x25, 0xFF, 0x7F, 0x40, 0x11, 0xE0, 0x01, 0xE0, 0x02, 0x7B, 0x57, 0xDF, 0x0D, 0xFF, 0x03], # Luigi
|
||||
[0x5F, 0x63, 0x1D, 0x58, 0x29, 0x25, 0xFF, 0x7F, 0x08, 0x00, 0x16, 0x02, 0x1F, 0x03, 0x7B, 0x57, 0xDF, 0x0D, 0xFF, 0x03], # Wario
|
||||
[0x5F, 0x63, 0x1D, 0x58, 0x29, 0x25, 0xFF, 0x7F, 0x08, 0x00, 0x0D, 0x58, 0x12, 0x7C, 0x7B, 0x57, 0xDF, 0x0D, 0xFF, 0x03], # Waluigi
|
||||
[0x5F, 0x63, 0x1D, 0x58, 0x29, 0x25, 0xFF, 0x7F, 0x08, 0x00, 0x00, 0x58, 0x00, 0x7C, 0x7B, 0x57, 0xDF, 0x0D, 0xFF, 0x03], # Geno
|
||||
[0x5F, 0x63, 0x1D, 0x58, 0x29, 0x25, 0xFF, 0x7F, 0x08, 0x00, 0x16, 0x58, 0x1F, 0x7C, 0x7B, 0x57, 0xDF, 0x0D, 0xFF, 0x03], # Princess
|
||||
[0x5F, 0x63, 0x1D, 0x58, 0x29, 0x25, 0xFF, 0x7F, 0x08, 0x00, 0x80, 0x00, 0xE0, 0x00, 0x7B, 0x57, 0xDF, 0x0D, 0xFF, 0x03], # Dark
|
||||
[0x5F, 0x63, 0x1D, 0x58, 0x29, 0x25, 0xFF, 0x7F, 0x08, 0x00, 0x5F, 0x01, 0xFF, 0x01, 0x7B, 0x57, 0xDF, 0x0D, 0xFF, 0x03], # Sponge
|
||||
]
|
||||
|
||||
ow_mario_palettes = [
|
||||
[0x16, 0x00, 0x1F, 0x00], # Mario
|
||||
[0x80, 0x02, 0xE0, 0x03], # Luigi
|
||||
[0x16, 0x02, 0x1F, 0x03], # Wario
|
||||
[0x0D, 0x58, 0x12, 0x7C], # Waluigi
|
||||
[0x00, 0x58, 0x00, 0x7C], # Geno
|
||||
[0x16, 0x58, 0x1F, 0x7C], # Princess
|
||||
[0x80, 0x00, 0xE0, 0x00], # Dark
|
||||
[0x5F, 0x01, 0xFF, 0x01], # Sponge
|
||||
]
|
||||
|
||||
level_music_address_data = [
|
||||
0x284DB,
|
||||
0x284DC,
|
||||
0x284DD,
|
||||
0x284DE,
|
||||
0x284DF,
|
||||
0x284E0,
|
||||
0x284E1,
|
||||
0x284E2,
|
||||
]
|
||||
|
||||
level_music_value_data = [
|
||||
0x02,
|
||||
0x06,
|
||||
0x01,
|
||||
0x08,
|
||||
0x07,
|
||||
0x03,
|
||||
0x05,
|
||||
0x12,
|
||||
]
|
||||
|
||||
ow_music_address_data = [
|
||||
[0x25BC8, 0x20D8A],
|
||||
[0x25BC9, 0x20D8B],
|
||||
[0x25BCA, 0x20D8C],
|
||||
[0x25BCB, 0x20D8D],
|
||||
[0x25BCC, 0x20D8E],
|
||||
[0x25BCD, 0x20D8F],
|
||||
[0x25BCE, 0x20D90],
|
||||
[0x16C7]
|
||||
]
|
||||
|
||||
ow_music_value_data = [
|
||||
0x02,
|
||||
0x03,
|
||||
0x04,
|
||||
0x06,
|
||||
0x07,
|
||||
0x09,
|
||||
0x05,
|
||||
0x01,
|
||||
]
|
||||
|
||||
valid_foreground_palettes = {
|
||||
0x00: [0x00, 0x01, 0x03, 0x04, 0x05, 0x07], # Normal 1
|
||||
0x01: [0x03, 0x04, 0x05, 0x07], # Castle 1
|
||||
0x02: [0x01, 0x02, 0x03, 0x04, 0x05, 0x07], # Rope 1
|
||||
0x03: [0x02, 0x03, 0x04, 0x05, 0x07], # Underground 1
|
||||
0x04: [0x01, 0x02, 0x03, 0x04, 0x05, 0x07], # Switch Palace 1
|
||||
0x05: [0x04, 0x05], # Ghost House 1
|
||||
0x06: [0x01, 0x02, 0x03, 0x04, 0x05, 0x07], # Rope 2
|
||||
0x07: [0x00, 0x01, 0x03, 0x04, 0x05, 0x07], # Normal 2
|
||||
0x08: [0x01, 0x02, 0x03, 0x04, 0x05, 0x07], # Rope 3
|
||||
0x09: [0x01, 0x02, 0x03, 0x04, 0x05, 0x07], # Underground 2
|
||||
0x0A: [0x01, 0x02, 0x03, 0x04, 0x05, 0x07], # Switch Palace 2
|
||||
0x0B: [0x03, 0x04, 0x05, 0x07], # Castle 2
|
||||
#0x0C: [], # Cloud/Forest
|
||||
0x0D: [0x04, 0x05], # Ghost House 2
|
||||
0x0E: [0x02, 0x03, 0x04, 0x05, 0x07], # Underground 3
|
||||
}
|
||||
|
||||
valid_background_palettes = {
|
||||
0x06861B: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07], # Ghost House Exit
|
||||
0xFFD900: [0x01], # P. Hills
|
||||
0xFFDAB9: [0x04], # Water
|
||||
0xFFDC71: [0x00, 0x01, 0x02, 0x03, 0x05, 0x06, 0x07], # Hills & Clouds
|
||||
0xFFDD44: [0x00, 0x01, 0x02, 0x03, 0x05, 0x06, 0x07], # Clouds
|
||||
0xFFDE54: [0x00, 0x01, 0x02, 0x03, 0x05, 0x06, 0x07], # Small Hills
|
||||
0xFFDF59: [0x00, 0x01, 0x02, 0x03, 0x05, 0x06, 0x07], # Mountains & Clouds
|
||||
0xFFE103: [0x00, 0x01, 0x02, 0x03, 0x05, 0x06, 0x07], # Castle Pillars
|
||||
0xFFE472: [0x00, 0x01, 0x02, 0x03, 0x05, 0x06, 0x07], # Big Hills
|
||||
0xFFE674: [0x00, 0x01, 0x02, 0x03, 0x05, 0x06, 0x07], # Bonus
|
||||
0xFFE684: [0x01, 0x03, 0x05, 0x06], # Stars
|
||||
0xFFE7C0: [0x00, 0x01, 0x02, 0x03, 0x05, 0x06, 0x07], # Mountains
|
||||
0xFFE8EE: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07], # Empty/Layer 2
|
||||
0xFFE8FE: [0x01, 0x06], # Cave
|
||||
0xFFEC82: [0x00, 0x02, 0x03, 0x05, 0x06, 0x07], # Bushes
|
||||
0xFFEF80: [0x01, 0x03, 0x05, 0x06], # Ghost House
|
||||
0xFFF175: [0x00, 0x01, 0x02, 0x03, 0x05, 0x06], # Ghost Ship
|
||||
0xFFF45A: [0x01, 0x03, 0x06], # Castle
|
||||
}
|
||||
|
||||
valid_background_colors = {
|
||||
0x06861B: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07], # Ghost House Exit
|
||||
0xFFD900: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07], # P. Hills
|
||||
0xFFDAB9: [0x02, 0x03, 0x05], # Water
|
||||
0xFFDC71: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07], # Hills & Clouds
|
||||
0xFFDD44: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07], # Clouds
|
||||
0xFFDE54: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07], # Small Hills
|
||||
0xFFDF59: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07], # Mountains & Clouds
|
||||
0xFFE103: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07], # Castle Pillars
|
||||
0xFFE472: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07], # Big Hills
|
||||
0xFFE674: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07], # Bonus
|
||||
0xFFE684: [0x02, 0x03, 0x04, 0x05], # Stars
|
||||
0xFFE7C0: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07], # Mountains
|
||||
0xFFE8EE: [0x03, 0x05], # Empty/Layer 2
|
||||
0xFFE8FE: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07], # Cave
|
||||
0xFFEC82: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07], # Bushes
|
||||
0xFFEF80: [0x03, 0x04], # Ghost House
|
||||
0xFFF175: [0x02, 0x03, 0x04, 0x05], # Ghost Ship
|
||||
0xFFF45A: [0x00, 0x01, 0x02, 0x03, 0x04, 0x05, 0x06, 0x07], # Castle
|
||||
}
|
||||
|
||||
def generate_shuffled_level_music(world, player):
|
||||
shuffled_level_music = level_music_value_data.copy()
|
||||
|
||||
if world.music_shuffle[player] == "consistent":
|
||||
world.random.shuffle(shuffled_level_music)
|
||||
elif world.music_shuffle[player] == "singularity":
|
||||
single_song = world.random.choice(shuffled_level_music)
|
||||
shuffled_level_music = [single_song for i in range(len(shuffled_level_music))]
|
||||
|
||||
return shuffled_level_music
|
||||
|
||||
def generate_shuffled_ow_music(world, player):
|
||||
shuffled_ow_music = ow_music_value_data.copy()
|
||||
|
||||
if world.music_shuffle[player] == "consistent" or world.music_shuffle[player] == "full":
|
||||
world.random.shuffle(shuffled_ow_music)
|
||||
elif world.music_shuffle[player] == "singularity":
|
||||
single_song = world.random.choice(shuffled_ow_music)
|
||||
shuffled_ow_music = [single_song for i in range(len(shuffled_ow_music))]
|
||||
|
||||
return shuffled_ow_music
|
||||
|
||||
def generate_shuffled_header_data(rom, world, player):
|
||||
if world.music_shuffle[player] != "full" and not world.foreground_palette_shuffle[player] and not world.background_palette_shuffle[player]:
|
||||
return
|
||||
|
||||
for level_id in range(0, 0x200):
|
||||
layer1_ptr_list = list(rom.read_bytes(0x2E000 + level_id * 3, 3))
|
||||
layer1_ptr = (layer1_ptr_list[2] << 16 | layer1_ptr_list[1] << 8 | layer1_ptr_list[0])
|
||||
|
||||
if layer1_ptr == 0x68000:
|
||||
# Unused Levels
|
||||
continue
|
||||
|
||||
if layer1_ptr >= 0x70000:
|
||||
layer1_ptr -= 0x8000
|
||||
|
||||
layer1_ptr -= 0x38000
|
||||
|
||||
level_header = list(rom.read_bytes(layer1_ptr, 5))
|
||||
|
||||
tileset = level_header[4] & 0x0F
|
||||
|
||||
if world.music_shuffle[player] == "full":
|
||||
level_header[2] &= 0x8F
|
||||
level_header[2] |= (world.random.randint(0, 7) << 5)
|
||||
|
||||
if (world.foreground_palette_shuffle[player] and tileset in valid_foreground_palettes):
|
||||
level_header[3] &= 0xF8
|
||||
level_header[3] |= world.random.choice(valid_foreground_palettes[tileset])
|
||||
|
||||
if world.background_palette_shuffle[player]:
|
||||
layer2_ptr_list = list(rom.read_bytes(0x2E600 + level_id * 3, 3))
|
||||
layer2_ptr = (layer2_ptr_list[2] << 16 | layer2_ptr_list[1] << 8 | layer2_ptr_list[0])
|
||||
|
||||
if layer2_ptr in valid_background_palettes:
|
||||
level_header[0] &= 0x1F
|
||||
level_header[0] |= (world.random.choice(valid_background_palettes[layer2_ptr]) << 5)
|
||||
|
||||
if layer2_ptr in valid_background_colors:
|
||||
level_header[1] &= 0x1F
|
||||
level_header[1] |= (world.random.choice(valid_background_colors[layer2_ptr]) << 5)
|
||||
|
||||
rom.write_bytes(layer1_ptr, bytes(level_header))
|
||||
@@ -0,0 +1,448 @@
|
||||
import logging
|
||||
import asyncio
|
||||
import time
|
||||
|
||||
from NetUtils import ClientStatus, color
|
||||
from worlds.AutoSNIClient import SNIClient
|
||||
from .Names.TextBox import generate_received_text
|
||||
|
||||
snes_logger = logging.getLogger("SNES")
|
||||
|
||||
# FXPAK Pro protocol memory mapping used by SNI
|
||||
ROM_START = 0x000000
|
||||
WRAM_START = 0xF50000
|
||||
WRAM_SIZE = 0x20000
|
||||
SRAM_START = 0xE00000
|
||||
|
||||
SMW_ROMHASH_START = 0x7FC0
|
||||
ROMHASH_SIZE = 0x15
|
||||
|
||||
SMW_PROGRESS_DATA = WRAM_START + 0x1F02
|
||||
SMW_DRAGON_COINS_DATA = WRAM_START + 0x1F2F
|
||||
SMW_PATH_DATA = WRAM_START + 0x1EA2
|
||||
SMW_EVENT_ROM_DATA = ROM_START + 0x2D608
|
||||
SMW_ACTIVE_LEVEL_DATA = ROM_START + 0x37F70
|
||||
|
||||
SMW_GOAL_DATA = ROM_START + 0x01BFA0
|
||||
SMW_REQUIRED_BOSSES_DATA = ROM_START + 0x01BFA1
|
||||
SMW_REQUIRED_EGGS_DATA = ROM_START + 0x01BFA2
|
||||
SMW_SEND_MSG_DATA = ROM_START + 0x01BFA3
|
||||
SMW_RECEIVE_MSG_DATA = ROM_START + 0x01BFA4
|
||||
SMW_DEATH_LINK_ACTIVE_ADDR = ROM_START + 0x01BFA5
|
||||
SMW_DRAGON_COINS_ACTIVE_ADDR = ROM_START + 0x01BFA6
|
||||
SMW_SWAMP_DONUT_GH_ADDR = ROM_START + 0x01BFA7
|
||||
|
||||
SMW_GAME_STATE_ADDR = WRAM_START + 0x100
|
||||
SMW_MARIO_STATE_ADDR = WRAM_START + 0x71
|
||||
SMW_BOSS_STATE_ADDR = WRAM_START + 0xD9B
|
||||
SMW_ACTIVE_BOSS_ADDR = WRAM_START + 0x13FC
|
||||
SMW_CURRENT_LEVEL_ADDR = WRAM_START + 0x13BF
|
||||
SMW_MESSAGE_BOX_ADDR = WRAM_START + 0x1426
|
||||
SMW_BONUS_STAR_ADDR = WRAM_START + 0xF48
|
||||
SMW_EGG_COUNT_ADDR = WRAM_START + 0x1F24
|
||||
SMW_BOSS_COUNT_ADDR = WRAM_START + 0x1F26
|
||||
SMW_NUM_EVENTS_ADDR = WRAM_START + 0x1F2E
|
||||
SMW_SFX_ADDR = WRAM_START + 0x1DFC
|
||||
SMW_PAUSE_ADDR = WRAM_START + 0x13D4
|
||||
SMW_MESSAGE_QUEUE_ADDR = WRAM_START + 0xC391
|
||||
|
||||
SMW_RECV_PROGRESS_ADDR = WRAM_START + 0x1F2B
|
||||
|
||||
SMW_GOAL_LEVELS = [0x28, 0x31, 0x32]
|
||||
SMW_INVALID_MARIO_STATES = [0x05, 0x06, 0x0A, 0x0C, 0x0D]
|
||||
SMW_BAD_TEXT_BOX_LEVELS = [0x26, 0x02, 0x4B]
|
||||
SMW_BOSS_STATES = [0x80, 0xC0, 0xC1]
|
||||
SMW_UNCOLLECTABLE_LEVELS = [0x25, 0x07, 0x0B, 0x40, 0x0E, 0x1F, 0x20, 0x1B, 0x1A, 0x35, 0x34, 0x31, 0x32]
|
||||
|
||||
|
||||
class SMWSNIClient(SNIClient):
|
||||
game = "Super Mario World"
|
||||
|
||||
async def deathlink_kill_player(self, ctx):
|
||||
from SNIClient import DeathState, snes_buffered_write, snes_flush_writes, snes_read
|
||||
game_state = await snes_read(ctx, SMW_GAME_STATE_ADDR, 0x1)
|
||||
if game_state[0] != 0x14:
|
||||
return
|
||||
|
||||
mario_state = await snes_read(ctx, SMW_MARIO_STATE_ADDR, 0x1)
|
||||
if mario_state[0] != 0x00:
|
||||
return
|
||||
|
||||
message_box = await snes_read(ctx, SMW_MESSAGE_BOX_ADDR, 0x1)
|
||||
if message_box[0] != 0x00:
|
||||
return
|
||||
|
||||
pause_state = await snes_read(ctx, SMW_PAUSE_ADDR, 0x1)
|
||||
if pause_state[0] != 0x00:
|
||||
return
|
||||
|
||||
snes_buffered_write(ctx, WRAM_START + 0x9D, bytes([0x30])) # Freeze Gameplay
|
||||
snes_buffered_write(ctx, WRAM_START + 0x1DFB, bytes([0x09])) # Death Music
|
||||
snes_buffered_write(ctx, WRAM_START + 0x0DDA, bytes([0xFF])) # Flush Music Buffer
|
||||
snes_buffered_write(ctx, WRAM_START + 0x1407, bytes([0x00])) # Flush Cape Fly Phase
|
||||
snes_buffered_write(ctx, WRAM_START + 0x140D, bytes([0x00])) # Flush Spin Jump Flag
|
||||
snes_buffered_write(ctx, WRAM_START + 0x188A, bytes([0x00])) # Flush Empty Byte because the game does it
|
||||
snes_buffered_write(ctx, WRAM_START + 0x7D, bytes([0x90])) # Mario Y Speed
|
||||
snes_buffered_write(ctx, WRAM_START + 0x1496, bytes([0x30])) # Death Timer
|
||||
snes_buffered_write(ctx, SMW_MARIO_STATE_ADDR, bytes([0x09])) # Mario State -> Dead
|
||||
|
||||
await snes_flush_writes(ctx)
|
||||
|
||||
ctx.death_state = DeathState.dead
|
||||
ctx.last_death_link = time.time()
|
||||
|
||||
|
||||
async def validate_rom(self, ctx):
|
||||
from SNIClient import snes_buffered_write, snes_flush_writes, snes_read
|
||||
|
||||
rom_name = await snes_read(ctx, SMW_ROMHASH_START, ROMHASH_SIZE)
|
||||
if rom_name is None or rom_name == bytes([0] * ROMHASH_SIZE) or rom_name[:3] != b"SMW":
|
||||
return False
|
||||
|
||||
ctx.game = self.game
|
||||
ctx.items_handling = 0b111 # remote items
|
||||
|
||||
receive_option = await snes_read(ctx, SMW_RECEIVE_MSG_DATA, 0x1)
|
||||
send_option = await snes_read(ctx, SMW_SEND_MSG_DATA, 0x1)
|
||||
|
||||
ctx.receive_option = receive_option[0]
|
||||
ctx.send_option = send_option[0]
|
||||
|
||||
ctx.allow_collect = True
|
||||
|
||||
death_link = await snes_read(ctx, SMW_DEATH_LINK_ACTIVE_ADDR, 1)
|
||||
if death_link:
|
||||
await ctx.update_death_link(bool(death_link[0] & 0b1))
|
||||
|
||||
ctx.rom = rom_name
|
||||
|
||||
return True
|
||||
|
||||
|
||||
def add_message_to_queue(self, new_message):
|
||||
|
||||
if not hasattr(self, "message_queue"):
|
||||
self.message_queue = []
|
||||
|
||||
self.message_queue.append(new_message)
|
||||
|
||||
|
||||
async def handle_message_queue(self, ctx):
|
||||
from SNIClient import snes_buffered_write, snes_flush_writes, snes_read
|
||||
|
||||
if not hasattr(self, "message_queue") or len(self.message_queue) == 0:
|
||||
return
|
||||
|
||||
game_state = await snes_read(ctx, SMW_GAME_STATE_ADDR, 0x1)
|
||||
if game_state[0] != 0x14:
|
||||
return
|
||||
|
||||
mario_state = await snes_read(ctx, SMW_MARIO_STATE_ADDR, 0x1)
|
||||
if mario_state[0] != 0x00:
|
||||
return
|
||||
|
||||
message_box = await snes_read(ctx, SMW_MESSAGE_BOX_ADDR, 0x1)
|
||||
if message_box[0] != 0x00:
|
||||
return
|
||||
|
||||
pause_state = await snes_read(ctx, SMW_PAUSE_ADDR, 0x1)
|
||||
if pause_state[0] != 0x00:
|
||||
return
|
||||
|
||||
current_level = await snes_read(ctx, SMW_CURRENT_LEVEL_ADDR, 0x1)
|
||||
if current_level[0] in SMW_BAD_TEXT_BOX_LEVELS:
|
||||
return
|
||||
|
||||
boss_state = await snes_read(ctx, SMW_BOSS_STATE_ADDR, 0x1)
|
||||
if boss_state[0] in SMW_BOSS_STATES:
|
||||
return
|
||||
|
||||
active_boss = await snes_read(ctx, SMW_ACTIVE_BOSS_ADDR, 0x1)
|
||||
if active_boss[0] != 0x00:
|
||||
return
|
||||
|
||||
next_message = self.message_queue.pop(0)
|
||||
|
||||
snes_buffered_write(ctx, SMW_MESSAGE_QUEUE_ADDR, bytes(next_message))
|
||||
snes_buffered_write(ctx, SMW_MESSAGE_BOX_ADDR, bytes([0x03]))
|
||||
snes_buffered_write(ctx, SMW_SFX_ADDR, bytes([0x22]))
|
||||
|
||||
await snes_flush_writes(ctx)
|
||||
|
||||
return
|
||||
|
||||
|
||||
async def game_watcher(self, ctx):
|
||||
from SNIClient import snes_buffered_write, snes_flush_writes, snes_read
|
||||
|
||||
game_state = await snes_read(ctx, SMW_GAME_STATE_ADDR, 0x1)
|
||||
mario_state = await snes_read(ctx, SMW_MARIO_STATE_ADDR, 0x1)
|
||||
if game_state is None:
|
||||
# We're not properly connected
|
||||
return
|
||||
elif game_state[0] >= 0x18:
|
||||
if not ctx.finished_game:
|
||||
current_level = await snes_read(ctx, SMW_CURRENT_LEVEL_ADDR, 0x1)
|
||||
|
||||
if current_level[0] in SMW_GOAL_LEVELS:
|
||||
await ctx.send_msgs([{"cmd": "StatusUpdate", "status": ClientStatus.CLIENT_GOAL}])
|
||||
ctx.finished_game = True
|
||||
return
|
||||
elif game_state[0] < 0x0B:
|
||||
# We haven't loaded a save file
|
||||
ctx.message_queue = []
|
||||
return
|
||||
elif mario_state[0] in SMW_INVALID_MARIO_STATES:
|
||||
# Mario can't come to the phone right now
|
||||
return
|
||||
|
||||
if "DeathLink" in ctx.tags and game_state[0] == 0x14 and ctx.last_death_link + 1 < time.time():
|
||||
currently_dead = mario_state[0] == 0x09
|
||||
await ctx.handle_deathlink_state(currently_dead)
|
||||
|
||||
# Check for Egg Hunt ending
|
||||
goal = await snes_read(ctx, SMW_GOAL_DATA, 0x1)
|
||||
if game_state[0] == 0x14 and goal[0] == 1:
|
||||
current_level = await snes_read(ctx, SMW_CURRENT_LEVEL_ADDR, 0x1)
|
||||
message_box = await snes_read(ctx, SMW_MESSAGE_BOX_ADDR, 0x1)
|
||||
egg_count = await snes_read(ctx, SMW_EGG_COUNT_ADDR, 0x1)
|
||||
required_egg_count = await snes_read(ctx, SMW_REQUIRED_EGGS_DATA, 0x1)
|
||||
|
||||
if current_level[0] == 0x28 and message_box[0] == 0x01 and egg_count[0] >= required_egg_count[0]:
|
||||
snes_buffered_write(ctx, WRAM_START + 0x13C6, bytes([0x08]))
|
||||
snes_buffered_write(ctx, WRAM_START + 0x13CE, bytes([0x01]))
|
||||
snes_buffered_write(ctx, WRAM_START + 0x1DE9, bytes([0x01]))
|
||||
snes_buffered_write(ctx, SMW_GAME_STATE_ADDR, bytes([0x18]))
|
||||
|
||||
await snes_flush_writes(ctx)
|
||||
return
|
||||
|
||||
egg_count = await snes_read(ctx, SMW_EGG_COUNT_ADDR, 0x1)
|
||||
boss_count = await snes_read(ctx, SMW_BOSS_COUNT_ADDR, 0x1)
|
||||
display_count = await snes_read(ctx, SMW_BONUS_STAR_ADDR, 0x1)
|
||||
|
||||
if goal[0] == 0 and boss_count[0] > display_count[0]:
|
||||
snes_buffered_write(ctx, SMW_BONUS_STAR_ADDR, bytes([boss_count[0]]))
|
||||
await snes_flush_writes(ctx)
|
||||
elif goal[0] == 1 and egg_count[0] > display_count[0]:
|
||||
snes_buffered_write(ctx, SMW_BONUS_STAR_ADDR, bytes([egg_count[0]]))
|
||||
await snes_flush_writes(ctx)
|
||||
|
||||
await self.handle_message_queue(ctx)
|
||||
|
||||
new_checks = []
|
||||
event_data = await snes_read(ctx, SMW_EVENT_ROM_DATA, 0x60)
|
||||
progress_data = bytearray(await snes_read(ctx, SMW_PROGRESS_DATA, 0x0F))
|
||||
dragon_coins_data = bytearray(await snes_read(ctx, SMW_DRAGON_COINS_DATA, 0x0C))
|
||||
dragon_coins_active = await snes_read(ctx, SMW_DRAGON_COINS_ACTIVE_ADDR, 0x1)
|
||||
from worlds.smw.Rom import item_rom_data, ability_rom_data
|
||||
from worlds.smw.Levels import location_id_to_level_id, level_info_dict
|
||||
from worlds import AutoWorldRegister
|
||||
for loc_name, level_data in location_id_to_level_id.items():
|
||||
loc_id = AutoWorldRegister.world_types[ctx.game].location_name_to_id[loc_name]
|
||||
if loc_id not in ctx.locations_checked:
|
||||
|
||||
event_id = event_data[level_data[0]]
|
||||
|
||||
if level_data[1] == 2:
|
||||
# Dragon Coins Check
|
||||
if not dragon_coins_active or dragon_coins_active[0] == 0:
|
||||
continue
|
||||
|
||||
progress_byte = (level_data[0] // 8)
|
||||
progress_bit = 7 - (level_data[0] % 8)
|
||||
|
||||
data = dragon_coins_data[progress_byte]
|
||||
masked_data = data & (1 << progress_bit)
|
||||
bit_set = (masked_data != 0)
|
||||
|
||||
if bit_set:
|
||||
new_checks.append(loc_id)
|
||||
else:
|
||||
event_id_value = event_id + level_data[1]
|
||||
|
||||
progress_byte = (event_id_value // 8)
|
||||
progress_bit = 7 - (event_id_value % 8)
|
||||
|
||||
data = progress_data[progress_byte]
|
||||
masked_data = data & (1 << progress_bit)
|
||||
bit_set = (masked_data != 0)
|
||||
|
||||
if bit_set:
|
||||
new_checks.append(loc_id)
|
||||
|
||||
verify_game_state = await snes_read(ctx, SMW_GAME_STATE_ADDR, 0x1)
|
||||
if verify_game_state is None or verify_game_state[0] < 0x0B or verify_game_state[0] > 0x29:
|
||||
# We have somehow exited the save file (or worse)
|
||||
print("Exit Save File")
|
||||
return
|
||||
|
||||
rom = await snes_read(ctx, SMW_ROMHASH_START, ROMHASH_SIZE)
|
||||
if rom != ctx.rom:
|
||||
ctx.rom = None
|
||||
print("Exit ROM")
|
||||
# We have somehow loaded a different ROM
|
||||
return
|
||||
|
||||
for new_check_id in new_checks:
|
||||
ctx.locations_checked.add(new_check_id)
|
||||
location = ctx.location_names[new_check_id]
|
||||
snes_logger.info(
|
||||
f'New Check: {location} ({len(ctx.locations_checked)}/{len(ctx.missing_locations) + len(ctx.checked_locations)})')
|
||||
await ctx.send_msgs([{"cmd": 'LocationChecks', "locations": [new_check_id]}])
|
||||
|
||||
if game_state[0] != 0x14:
|
||||
# Don't receive items or collect locations outside of in-level mode
|
||||
return
|
||||
|
||||
recv_count = await snes_read(ctx, SMW_RECV_PROGRESS_ADDR, 1)
|
||||
recv_index = recv_count[0]
|
||||
|
||||
if recv_index < len(ctx.items_received):
|
||||
item = ctx.items_received[recv_index]
|
||||
recv_index += 1
|
||||
logging.info('Received %s from %s (%s) (%d/%d in list)' % (
|
||||
color(ctx.item_names[item.item], 'red', 'bold'),
|
||||
color(ctx.player_names[item.player], 'yellow'),
|
||||
ctx.location_names[item.location], recv_index, len(ctx.items_received)))
|
||||
|
||||
if ctx.receive_option == 1 or (ctx.receive_option == 2 and ((item.flags & 1) != 0)):
|
||||
if item.item != 0xBC0012:
|
||||
# Don't send messages for Boss Tokens
|
||||
item_name = ctx.item_names[item.item]
|
||||
player_name = ctx.player_names[item.player]
|
||||
|
||||
receive_message = generate_received_text(item_name, player_name)
|
||||
self.add_message_to_queue(receive_message)
|
||||
|
||||
snes_buffered_write(ctx, SMW_RECV_PROGRESS_ADDR, bytes([recv_index]))
|
||||
if item.item in item_rom_data:
|
||||
item_count = await snes_read(ctx, WRAM_START + item_rom_data[item.item][0], 0x1)
|
||||
increment = item_rom_data[item.item][1]
|
||||
|
||||
new_item_count = item_count[0]
|
||||
if increment > 1:
|
||||
new_item_count = increment
|
||||
else:
|
||||
new_item_count += increment
|
||||
|
||||
if verify_game_state[0] == 0x14 and len(item_rom_data[item.item]) > 2:
|
||||
snes_buffered_write(ctx, SMW_SFX_ADDR, bytes([item_rom_data[item.item][2]]))
|
||||
|
||||
snes_buffered_write(ctx, WRAM_START + item_rom_data[item.item][0], bytes([new_item_count]))
|
||||
elif item.item in ability_rom_data:
|
||||
# Handle Upgrades
|
||||
for rom_data in ability_rom_data[item.item]:
|
||||
data = await snes_read(ctx, WRAM_START + rom_data[0], 1)
|
||||
masked_data = data[0] | (1 << rom_data[1])
|
||||
snes_buffered_write(ctx, WRAM_START + rom_data[0], bytes([masked_data]))
|
||||
snes_buffered_write(ctx, SMW_SFX_ADDR, bytes([0x3E])) # SMW_TODO: Custom sounds for each
|
||||
elif item.item == 0xBC000A:
|
||||
# Handle Progressive Powerup
|
||||
data = await snes_read(ctx, WRAM_START + 0x1F2D, 1)
|
||||
mushroom_data = data[0] & (1 << 0)
|
||||
fire_flower_data = data[0] & (1 << 1)
|
||||
cape_data = data[0] & (1 << 2)
|
||||
if mushroom_data == 0:
|
||||
masked_data = data[0] | (1 << 0)
|
||||
snes_buffered_write(ctx, WRAM_START + 0x1F2D, bytes([masked_data]))
|
||||
snes_buffered_write(ctx, SMW_SFX_ADDR, bytes([0x3E]))
|
||||
elif fire_flower_data == 0:
|
||||
masked_data = data[0] | (1 << 1)
|
||||
snes_buffered_write(ctx, WRAM_START + 0x1F2D, bytes([masked_data]))
|
||||
snes_buffered_write(ctx, SMW_SFX_ADDR, bytes([0x3E]))
|
||||
elif cape_data == 0:
|
||||
masked_data = data[0] | (1 << 2)
|
||||
snes_buffered_write(ctx, WRAM_START + 0x1F2D, bytes([masked_data]))
|
||||
snes_buffered_write(ctx, SMW_SFX_ADDR, bytes([0x41]))
|
||||
else:
|
||||
# Extra Powerup?
|
||||
pass
|
||||
elif item.item == 0xBC0015:
|
||||
# Handle Literature Trap
|
||||
from .Names.LiteratureTrap import lit_trap_text_list
|
||||
import random
|
||||
rand_trap = random.choice(lit_trap_text_list)
|
||||
|
||||
for message in rand_trap:
|
||||
self.add_message_to_queue(message)
|
||||
|
||||
await snes_flush_writes(ctx)
|
||||
|
||||
# Handle Collected Locations
|
||||
new_events = 0
|
||||
path_data = bytearray(await snes_read(ctx, SMW_PATH_DATA, 0x60))
|
||||
donut_gh_swapped = await snes_read(ctx, SMW_SWAMP_DONUT_GH_ADDR, 0x1)
|
||||
new_dragon_coin = False
|
||||
for loc_id in ctx.checked_locations:
|
||||
if loc_id not in ctx.locations_checked:
|
||||
ctx.locations_checked.add(loc_id)
|
||||
loc_name = ctx.location_names[loc_id]
|
||||
|
||||
if loc_name not in location_id_to_level_id:
|
||||
continue
|
||||
|
||||
level_data = location_id_to_level_id[loc_name]
|
||||
|
||||
if level_data[1] == 2:
|
||||
# Dragon Coins Check
|
||||
|
||||
progress_byte = (level_data[0] // 8)
|
||||
progress_bit = 7 - (level_data[0] % 8)
|
||||
|
||||
data = dragon_coins_data[progress_byte]
|
||||
new_data = data | (1 << progress_bit)
|
||||
dragon_coins_data[progress_byte] = new_data
|
||||
|
||||
new_dragon_coin = True
|
||||
else:
|
||||
if level_data[0] in SMW_UNCOLLECTABLE_LEVELS:
|
||||
continue
|
||||
|
||||
event_id = event_data[level_data[0]]
|
||||
event_id_value = event_id + level_data[1]
|
||||
|
||||
progress_byte = (event_id_value // 8)
|
||||
progress_bit = 7 - (event_id_value % 8)
|
||||
|
||||
data = progress_data[progress_byte]
|
||||
masked_data = data & (1 << progress_bit)
|
||||
bit_set = (masked_data != 0)
|
||||
|
||||
if bit_set:
|
||||
continue
|
||||
|
||||
new_events += 1
|
||||
new_data = data | (1 << progress_bit)
|
||||
progress_data[progress_byte] = new_data
|
||||
|
||||
tile_id = await snes_read(ctx, SMW_ACTIVE_LEVEL_DATA + level_data[0], 0x1)
|
||||
|
||||
level_info = level_info_dict[tile_id[0]]
|
||||
|
||||
path = level_info.exit1Path if level_data[1] == 0 else level_info.exit2Path
|
||||
|
||||
if donut_gh_swapped[0] != 0 and tile_id[0] == 0x04:
|
||||
# Handle Swapped Donut GH Exits
|
||||
path = level_info.exit2Path if level_data[1] == 0 else level_info.exit1Path
|
||||
|
||||
if not path:
|
||||
continue
|
||||
|
||||
this_end_path = path_data[tile_id[0]]
|
||||
new_data = this_end_path | path.thisEndDirection
|
||||
path_data[tile_id[0]] = new_data
|
||||
|
||||
other_end_path = path_data[path.otherLevelID]
|
||||
new_data = other_end_path | path.otherEndDirection
|
||||
path_data[path.otherLevelID] = new_data
|
||||
|
||||
if new_dragon_coin:
|
||||
snes_buffered_write(ctx, SMW_DRAGON_COINS_DATA, bytes(dragon_coins_data))
|
||||
if new_events > 0:
|
||||
snes_buffered_write(ctx, SMW_PROGRESS_DATA, bytes(progress_data))
|
||||
snes_buffered_write(ctx, SMW_PATH_DATA, bytes(path_data))
|
||||
old_events = await snes_read(ctx, SMW_NUM_EVENTS_ADDR, 0x1)
|
||||
snes_buffered_write(ctx, SMW_NUM_EVENTS_ADDR, bytes([old_events[0] + new_events]))
|
||||
|
||||
await snes_flush_writes(ctx)
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user