Merge branch 'main' into feature/ds3_use_slotdata

# Conflicts:
#	worlds/dark_souls_3/__init__.py
This commit is contained in:
Marechal-l
2022-10-28 19:40:24 +02:00
258 changed files with 28546 additions and 4330 deletions
+42
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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")
+1 -1
View File
@@ -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]:
+693
View File
@@ -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)
+15 -8
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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":
+2 -2
View File
@@ -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**
+1 -1
View File
@@ -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**
+3 -3
View File
@@ -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.
+3 -3
View File
@@ -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.
+33
View File
@@ -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")
+13
View File
@@ -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.
+24
View File
@@ -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
+17
View File
@@ -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
+17
View File
@@ -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
+14 -22
View File
@@ -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"
+3 -1
View File
@@ -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}
+9 -7
View File
@@ -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
View File
@@ -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)
View File
+5 -4
View File
@@ -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
View File
@@ -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
+2 -3
View File
@@ -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**
+5 -2
View File
@@ -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
+4 -3
View File
@@ -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 #}
+25 -8
View File
@@ -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
View File
@@ -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,
+8 -1
View File
@@ -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.
+42 -41
View File
@@ -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.
+46 -51
View File
@@ -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".
+1 -1
View File
@@ -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
+94
View File
@@ -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
}
+243
View File
@@ -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'}
}
+383
View File
@@ -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}
}
+41
View File
@@ -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
}
+433
View File
@@ -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))
+246
View File
@@ -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"
+17
View File
@@ -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.
+35
View File
@@ -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.
+1 -1
View File
@@ -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)
+1 -1
View File
@@ -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')))
+51 -17
View File
@@ -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
View File
@@ -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}
+1 -1
View File
@@ -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,
+152
View File
@@ -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,
}
+15
View File
@@ -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
+110
View File
@@ -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()})
+349
View File
@@ -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" ,
}
+510
View File
@@ -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
+84
View File
@@ -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:
![Sign-In Screen](https://i.imgur.com/goMy7o2.png)
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.
+21
View File
@@ -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.
+252
View File
@@ -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.
+84
View File
@@ -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.
+176
View File
@@ -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
+73
View File
@@ -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)
+480
View File
@@ -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
+305
View File
@@ -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)
+631
View File
@@ -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)
+588
View File
@@ -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,
}
+165
View File
@@ -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)
+147
View File
@@ -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
+6 -6
View File
@@ -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)
+12 -12
View File
@@ -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
View File
+5 -5
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
View File
@@ -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
+257
View File
@@ -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
View File
@@ -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
View File
@@ -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?
+7 -13
View File
@@ -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
+158
View File
@@ -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
View File
@@ -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
View File
@@ -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)
+2 -3
View File
@@ -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**
+3 -2
View File
@@ -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):
+9 -8
View File
@@ -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))
+2 -2
View File
@@ -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)
+17 -4
View File
@@ -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.
+202
View File
@@ -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))
+448
View File
@@ -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